diff --git a/PlexRequests.Core.Migration/Migrations/Version1100.cs b/PlexRequests.Core.Migration/Migrations/Version1100.cs index 3e6050fa9..3a7094954 100644 --- a/PlexRequests.Core.Migration/Migrations/Version1100.cs +++ b/PlexRequests.Core.Migration/Migrations/Version1100.cs @@ -43,8 +43,8 @@ namespace PlexRequests.Core.Migration.Migrations [Migration(11000, "v1.10.0.0")] public class Version1100 : BaseMigration, IMigration { - public Version1100(IUserRepository userRepo, IRequestService requestService, ISettingsService log, IPlexApi plexApi, ISettingsService plexService, IRepository plexusers, - ISettingsService prSettings, ISettingsService umSettings) + public Version1100(IUserRepository userRepo, IRequestService requestService, ISettingsService log, IPlexApi plexApi, ISettingsService plexService, IPlexUserRepository plexusers, + ISettingsService prSettings, ISettingsService umSettings, ISettingsService sjs) { UserRepo = userRepo; RequestService = requestService; @@ -54,6 +54,7 @@ namespace PlexRequests.Core.Migration.Migrations PlexUsers = plexusers; PlexRequestSettings = prSettings; UserManagementSettings = umSettings; + ScheduledJobSettings = sjs; } public int Version => 11000; private IUserRepository UserRepo { get; } @@ -61,9 +62,10 @@ namespace PlexRequests.Core.Migration.Migrations private ISettingsService Log { get; } private IPlexApi PlexApi { get; } private ISettingsService PlexSettings { get; } - private IRepository PlexUsers { get; } + private IPlexUserRepository PlexUsers { get; } private ISettingsService PlexRequestSettings { get; } private ISettingsService UserManagementSettings { get; } + private ISettingsService ScheduledJobSettings { get; } public void Start(IDbConnection con) { @@ -74,10 +76,21 @@ namespace PlexRequests.Core.Migration.Migrations ResetLogLevel(); UpdatePlexUsers(); PopulateDefaultUserManagementSettings(); + UpdateScheduledJobs(); UpdateSchema(con, Version); } + private void UpdateScheduledJobs() + { + var settings = ScheduledJobSettings.GetSettings(); + + settings.PlexUserChecker = 24; + settings.PlexContentCacher = 60; + + ScheduledJobSettings.SaveSettings(settings); + } + private void PopulateDefaultUserManagementSettings() { var plexRequestSettings = PlexRequestSettings.GetSettings(); @@ -147,6 +160,9 @@ namespace PlexRequests.Core.Migration.Migrations Permissions = permissions, Features = 0, UserAlias = string.Empty, + EmailAddress = user.Email, + Username = user.Username, + LoginId = Guid.NewGuid().ToString() }; PlexUsers.Insert(m); @@ -171,6 +187,8 @@ namespace PlexRequests.Core.Migration.Migrations con.AlterTable("PlexUsers", "ADD", "Permissions", true, "INTEGER"); con.AlterTable("PlexUsers", "ADD", "Features", true, "INTEGER"); + con.AlterTable("PlexUsers", "ADD", "Username", true, "VARCHAR(100)"); + con.AlterTable("PlexUsers", "ADD", "EmailAddress", true, "VARCHAR(100)"); //https://image.tmdb.org/t/p/w150/https://image.tmdb.org/t/p/w150//aqhAqttDq7zgsTaBHtCD8wmTk6k.jpg diff --git a/PlexRequests.Core/SettingModels/ScheduledJobsSettings.cs b/PlexRequests.Core/SettingModels/ScheduledJobsSettings.cs index be4fe5409..d0e6b05e1 100644 --- a/PlexRequests.Core/SettingModels/ScheduledJobsSettings.cs +++ b/PlexRequests.Core/SettingModels/ScheduledJobsSettings.cs @@ -44,5 +44,6 @@ namespace PlexRequests.Core.SettingModels public string RecentlyAddedCron { get; set; } public int FaultQueueHandler { get; set; } public int PlexContentCacher { get; set; } + public int PlexUserChecker { get; set; } } } \ No newline at end of file diff --git a/PlexRequests.Services/Jobs/JobNames.cs b/PlexRequests.Services/Jobs/JobNames.cs index 692295f47..d88cef62b 100644 --- a/PlexRequests.Services/Jobs/JobNames.cs +++ b/PlexRequests.Services/Jobs/JobNames.cs @@ -39,5 +39,7 @@ namespace PlexRequests.Services.Jobs public const string EpisodeCacher = "Plex Episode Cacher"; public const string RecentlyAddedEmail = "Recently Added Email Notification"; public const string FaultQueueHandler = "Request Fault Queue Handler"; + public const string PlexUserChecker = "Plex User Checker"; + } } \ No newline at end of file diff --git a/PlexRequests.Services/Jobs/PlexUserChecker.cs b/PlexRequests.Services/Jobs/PlexUserChecker.cs new file mode 100644 index 000000000..fb851a3f4 --- /dev/null +++ b/PlexRequests.Services/Jobs/PlexUserChecker.cs @@ -0,0 +1,164 @@ +#region Copyright + +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: StoreCleanup.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ + +#endregion + +using System; +using System.Linq; +using NLog; +using PlexRequests.Api.Interfaces; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; +using PlexRequests.Helpers; +using PlexRequests.Helpers.Permissions; +using PlexRequests.Services.Interfaces; +using PlexRequests.Store.Models; +using PlexRequests.Store.Repository; + +using Quartz; + +namespace PlexRequests.Services.Jobs +{ + public class PlexUserChecker : IJob + { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + public PlexUserChecker(IPlexUserRepository plexUsers, IPlexApi plexAPi, IJobRecord rec, ISettingsService plexSettings, ISettingsService prSettings) + { + Repo = plexUsers; + JobRecord = rec; + PlexApi = plexAPi; + PlexSettings = plexSettings; + PlexRequestSettings = prSettings; + } + + private IJobRecord JobRecord { get; } + private IPlexApi PlexApi { get; } + private IPlexUserRepository Repo { get; } + private ISettingsService PlexSettings { get; } + private ISettingsService PlexRequestSettings { get; } + + + public void Execute(IJobExecutionContext context) + { + JobRecord.SetRunning(true, JobNames.PlexUserChecker); + + try + { + var settings = PlexSettings.GetSettings(); + if (string.IsNullOrEmpty(settings.PlexAuthToken)) + { + return; + } + var plexUsers = PlexApi.GetUsers(settings.PlexAuthToken); + var prSettings = PlexRequestSettings.GetSettings(); + + var dbUsers = Repo.GetAll().ToList(); + foreach (var user in plexUsers.User) + { + var dbUser = dbUsers.FirstOrDefault(x => x.PlexUserId == user.Id); + if (dbUser != null) + { + var needToUpdate = false; + + // Do we need up update any info? + if (dbUser.EmailAddress != user.Email) + { + dbUser.EmailAddress = user.Email; + needToUpdate = true; + } + if (dbUser.Username != user.Username) + { + dbUser.Username = user.Username; + needToUpdate = true; + } + + if (needToUpdate) + { + Repo.Update(dbUser); + } + + continue; + } + + int permissions = 0; + if (prSettings.SearchForMovies) + { + permissions = (int)Permissions.RequestMovie; + } + if (prSettings.SearchForTvShows) + { + permissions += (int)Permissions.RequestTvShow; + } + if (prSettings.SearchForMusic) + { + permissions += (int)Permissions.RequestMusic; + } + if (!prSettings.RequireMovieApproval) + { + permissions += (int)Permissions.AutoApproveMovie; + } + if (!prSettings.RequireTvShowApproval) + { + permissions += (int)Permissions.AutoApproveTv; + } + if (!prSettings.RequireMusicApproval) + { + permissions += (int)Permissions.AutoApproveAlbum; + } + + // Add report Issues + + permissions += (int)Permissions.ReportIssue; + + var m = new PlexUsers + { + PlexUserId = user.Id, + Permissions = permissions, + Features = 0, + UserAlias = string.Empty, + EmailAddress = user.Email, + Username = user.Username, + LoginId = Guid.NewGuid().ToString() + }; + + Repo.Insert(m); + } + + } + catch (Exception e) + { + Log.Error(e); + } + finally + { + JobRecord.SetRunning(false, JobNames.PlexUserChecker); + JobRecord.Record(JobNames.PlexUserChecker); + } + } + } +} \ No newline at end of file diff --git a/PlexRequests.Services/PlexRequests.Services.csproj b/PlexRequests.Services/PlexRequests.Services.csproj index 5a0468793..d22ed182b 100644 --- a/PlexRequests.Services/PlexRequests.Services.csproj +++ b/PlexRequests.Services/PlexRequests.Services.csproj @@ -93,6 +93,7 @@ + diff --git a/PlexRequests.Store/Models/PlexUsers.cs b/PlexRequests.Store/Models/PlexUsers.cs index c860b9551..c4f8a0296 100644 --- a/PlexRequests.Store/Models/PlexUsers.cs +++ b/PlexRequests.Store/Models/PlexUsers.cs @@ -25,6 +25,7 @@ // ************************************************************************/ #endregion +using System; using Dapper.Contrib.Extensions; namespace PlexRequests.Store.Models @@ -36,5 +37,8 @@ namespace PlexRequests.Store.Models public string UserAlias { get; set; } public int Permissions { get; set; } public int Features { get; set; } + public string Username { get; set; } + public string EmailAddress { get; set; } + public string LoginId { get; set; } } } \ No newline at end of file diff --git a/PlexRequests.Store/PlexRequests.Store.csproj b/PlexRequests.Store/PlexRequests.Store.csproj index c3e81297a..ce6546de8 100644 --- a/PlexRequests.Store/PlexRequests.Store.csproj +++ b/PlexRequests.Store/PlexRequests.Store.csproj @@ -88,6 +88,7 @@ + diff --git a/PlexRequests.Store/Repository/PlexUserRepository.cs b/PlexRequests.Store/Repository/PlexUserRepository.cs new file mode 100644 index 000000000..177dab2b5 --- /dev/null +++ b/PlexRequests.Store/Repository/PlexUserRepository.cs @@ -0,0 +1,117 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: UserRepository.cs +// Created By: +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; +using Dapper; +using Dapper.Contrib.Extensions; +using PlexRequests.Helpers; +using PlexRequests.Store.Models; + +namespace PlexRequests.Store.Repository +{ + public class PlexUserRepository : BaseGenericRepository, IPlexUserRepository + { + public PlexUserRepository(ISqliteConfiguration config, ICacheProvider cache) : base(config,cache) + { + DbConfig = config; + Cache = cache; + } + + private ISqliteConfiguration DbConfig { get; } + private ICacheProvider Cache { get; } + private IDbConnection Db => DbConfig.DbConnection(); + + public PlexUsers GetUser(string userGuid) + { + var sql = @"SELECT * FROM PlexUsers + WHERE PlexUserId = @UserGuid"; + return Db.QueryFirstOrDefault(sql, new {UserGuid = userGuid}); + } + + public PlexUsers GetUserByUsername(string username) + { + var sql = @"SELECT * FROM PlexUsers + WHERE Username = @UserName"; + return Db.QueryFirstOrDefault(sql, new {UserName = username}); + } + + public async Task GetUserAsync(string userguid) + { + var sql = @"SELECT * FROM PlexUsers + WHERE PlexUserId = @UserGuid"; + return await Db.QueryFirstOrDefaultAsync(sql, new {UserGuid = userguid}); + } + + #region abstract implimentation + [Obsolete] + public override PlexUsers Get(string id) + { + throw new System.NotImplementedException(); + } + + [Obsolete] + public override Task GetAsync(int id) + { + throw new System.NotImplementedException(); + } + + [Obsolete] + public override PlexUsers Get(int id) + { + throw new System.NotImplementedException(); + } + + [Obsolete] + public override Task GetAsync(string id) + { + throw new System.NotImplementedException(); + } + + #endregion + } + + + public interface IPlexUserRepository + { + PlexUsers GetUser(string userGuid); + PlexUsers GetUserByUsername(string username); + Task GetUserAsync(string userguid); + IEnumerable Custom(Func> func); + long Insert(PlexUsers entity); + void Delete(PlexUsers entity); + IEnumerable GetAll(); + bool UpdateAll(IEnumerable entity); + bool Update(PlexUsers entity); + Task> GetAllAsync(); + Task UpdateAsync(PlexUsers users); + Task InsertAsync(PlexUsers users); + } +} + diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql index 70a71fcdd..e1f5e6b44 100644 --- a/PlexRequests.Store/SqlTables.sql +++ b/PlexRequests.Store/SqlTables.sql @@ -116,8 +116,11 @@ CREATE TABLE IF NOT EXISTS PlexUsers Id INTEGER PRIMARY KEY AUTOINCREMENT, PlexUserId varchar(100) NOT NULL, UserAlias varchar(100) NOT NULL, - Permissions INTEGER, - Features INTEGER + Permissions INTEGER, + Features INTEGER, + Username VARCHAR(100), + EmailAddress VARCHAR(100), + LoginId VARCHAR(100) ); CREATE UNIQUE INDEX IF NOT EXISTS PlexUsers_Id ON PlexUsers (Id); diff --git a/PlexRequests.UI/Authentication/CustomAuthenticationConfiguration.cs b/PlexRequests.UI/Authentication/CustomAuthenticationConfiguration.cs new file mode 100644 index 000000000..274267f36 --- /dev/null +++ b/PlexRequests.UI/Authentication/CustomAuthenticationConfiguration.cs @@ -0,0 +1,93 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: CustomAuthenticationConfiguration.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using Nancy.Cryptography; +using PlexRequests.Store.Repository; + +namespace PlexRequests.UI.Authentication +{ + public class CustomAuthenticationConfiguration + { + internal const string DefaultRedirectQuerystringKey = "returnUrl"; + + /// + /// Gets or sets the forms authentication query string key for storing the return url + /// + public string RedirectQuerystringKey { get; set; } + + /// + /// Gets or sets the redirect url for pages that require authentication + /// + public string RedirectUrl { get; set; } + + /// Gets or sets the username/identifier mapper + public IUserRepository LocalUserRepository { get; set; } + + public IPlexUserRepository PlexUserRepository { get; set; } + + /// Gets or sets RequiresSSL property + /// The flag that indicates whether SSL is required + public bool RequiresSSL { get; set; } + + /// + /// Gets or sets whether to redirect to login page during unauthorized access. + /// + public bool DisableRedirect { get; set; } + + /// Gets or sets the domain of the auth cookie + public string Domain { get; set; } + + /// Gets or sets the path of the auth cookie + public string Path { get; set; } + + /// Gets or sets the cryptography configuration + public CryptographyConfiguration CryptographyConfiguration { get; set; } + + /// + /// Gets a value indicating whether the configuration is valid or not. + /// + public virtual bool IsValid => (this.DisableRedirect || !string.IsNullOrEmpty(this.RedirectUrl)) && (this.LocalUserRepository != null && PlexUserRepository != null && this.CryptographyConfiguration != null) && (this.CryptographyConfiguration.EncryptionProvider != null && this.CryptographyConfiguration.HmacProvider != null); + + /// + /// Initializes a new instance of the class. + /// + public CustomAuthenticationConfiguration() + : this(CryptographyConfiguration.Default) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Cryptography configuration + public CustomAuthenticationConfiguration(CryptographyConfiguration cryptographyConfiguration) + { + this.CryptographyConfiguration = cryptographyConfiguration; + this.RedirectQuerystringKey = "returnUrl"; + } + } +} diff --git a/PlexRequests.UI/Authentication/CustomAuthenticationProvider.cs b/PlexRequests.UI/Authentication/CustomAuthenticationProvider.cs new file mode 100644 index 000000000..285f9c89e --- /dev/null +++ b/PlexRequests.UI/Authentication/CustomAuthenticationProvider.cs @@ -0,0 +1,409 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: CustomAuthenticationProvider.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using System.Linq; +using Nancy; +using Nancy.Authentication.Forms; +using Nancy.Bootstrapper; +using Nancy.Cookies; +using Nancy.Cryptography; +using Nancy.Extensions; +using Nancy.Helpers; +using Nancy.Security; +using PlexRequests.Core; +using PlexRequests.Helpers; + +namespace PlexRequests.UI.Authentication +{ + public class CustomAuthenticationProvider + { + private static string formsAuthenticationCookieName = "_ncfa"; + private static CustomAuthenticationConfiguration currentConfiguration; + + /// Gets or sets the forms authentication cookie name + public static string FormsAuthenticationCookieName + { + get + { + return CustomAuthenticationProvider.formsAuthenticationCookieName; + } + set + { + CustomAuthenticationProvider.formsAuthenticationCookieName = value; + } + } + + /// Enables forms authentication for the application + /// Pipelines to add handlers to (usually "this") + /// Forms authentication configuration + public static void Enable(IPipelines pipelines, CustomAuthenticationConfiguration configuration) + { + if (pipelines == null) + throw new ArgumentNullException("pipelines"); + if (configuration == null) + throw new ArgumentNullException("configuration"); + if (!configuration.IsValid) + throw new ArgumentException("Configuration is invalid", "configuration"); + CustomAuthenticationProvider.currentConfiguration = configuration; + pipelines.BeforeRequest.AddItemToStartOfPipeline(CustomAuthenticationProvider.GetLoadAuthenticationHook(configuration)); + if (configuration.DisableRedirect) + return; + pipelines.AfterRequest.AddItemToEndOfPipeline(CustomAuthenticationProvider.GetRedirectToLoginHook(configuration)); + } + + /// Enables forms authentication for a module + /// Module to add handlers to (usually "this") + /// Forms authentication configuration + public static void Enable(INancyModule module, CustomAuthenticationConfiguration configuration) + { + if (module == null) + throw new ArgumentNullException("module"); + if (configuration == null) + throw new ArgumentNullException("configuration"); + if (!configuration.IsValid) + throw new ArgumentException("Configuration is invalid", "configuration"); + module.RequiresAuthentication(); + CustomAuthenticationProvider.currentConfiguration = configuration; + module.Before.AddItemToStartOfPipeline(CustomAuthenticationProvider.GetLoadAuthenticationHook(configuration)); + if (configuration.DisableRedirect) + return; + module.After.AddItemToEndOfPipeline(CustomAuthenticationProvider.GetRedirectToLoginHook(configuration)); + } + /// + /// Creates a response that sets the authentication cookie and redirects + /// the user back to where they came from. + /// + /// Current context + /// User identifier guid + /// Optional expiry date for the cookie (for 'Remember me') + /// Url to redirect to if none in the querystring + /// Nancy response with redirect. + public static Response UserLoggedInRedirectResponse(NancyContext context, Guid userIdentifier, DateTime? cookieExpiry = null, string fallbackRedirectUrl = null) + { + var redirectUrl = fallbackRedirectUrl; + + if (string.IsNullOrEmpty(redirectUrl)) + { + redirectUrl = context.Request.Url.BasePath; + } + + if (string.IsNullOrEmpty(redirectUrl)) + { + redirectUrl = "/"; + } + + string redirectQuerystringKey = GetRedirectQuerystringKey(currentConfiguration); + + if (context.Request.Query[redirectQuerystringKey].HasValue) + { + var queryUrl = (string)context.Request.Query[redirectQuerystringKey]; + + if (context.IsLocalUrl(queryUrl)) + { + redirectUrl = queryUrl; + } + } + + var response = context.GetRedirect(redirectUrl); + var authenticationCookie = BuildCookie(userIdentifier, cookieExpiry, currentConfiguration); + response.WithCookie(authenticationCookie); + + return response; + } + /// + /// Logs the user in. + /// + /// User identifier guid + /// Optional expiry date for the cookie (for 'Remember me') + /// Nancy response with status + public static Response UserLoggedInResponse(Guid userIdentifier, DateTime? cookieExpiry = null) + { + var response = + (Response)HttpStatusCode.OK; + + var authenticationCookie = + BuildCookie(userIdentifier, cookieExpiry, currentConfiguration); + + response.WithCookie(authenticationCookie); + + return response; + } + + /// + /// Logs the user out and redirects them to a URL + /// + /// Current context + /// URL to redirect to + /// Nancy response + public static Response LogOutAndRedirectResponse(NancyContext context, string redirectUrl) + { + var response = context.GetRedirect(redirectUrl); + var authenticationCookie = BuildLogoutCookie(currentConfiguration); + response.WithCookie(authenticationCookie); + + return response; + } + + /// + /// Logs the user out. + /// + /// Nancy response + public static Response LogOutResponse() + { + var response = + (Response)HttpStatusCode.OK; + + var authenticationCookie = + BuildLogoutCookie(currentConfiguration); + + response.WithCookie(authenticationCookie); + + return response; + } + + /// + /// Gets the pre request hook for loading the authenticated user's details + /// from the cookie. + /// + /// Forms authentication configuration to use + /// Pre request hook delegate + private static Func GetLoadAuthenticationHook(CustomAuthenticationConfiguration configuration) + { + if (configuration == null) + { + throw new ArgumentNullException("configuration"); + } + + return context => + { + var userGuid = GetAuthenticatedUserFromCookie(context, configuration); + + if (userGuid != Guid.Empty) + { + var identity = new UserIdentity(); + + var plexUsers = configuration.PlexUserRepository.GetAll(); + var plexUser = plexUsers.FirstOrDefault(x => Guid.Parse(x.LoginId) == userGuid); + + if (plexUser != null) + { + identity.UserName = plexUser.Username; + } + + var localUsers = configuration.LocalUserRepository.GetAll(); + + var localUser = localUsers.FirstOrDefault(x => Guid.Parse(x.UserGuid) == userGuid); + if (localUser != null) + { + identity.UserName = localUser.UserName; + } + + context.CurrentUser = identity; + } + + return null; + }; + } + + /// + /// Gets the post request hook for redirecting to the login page + /// + /// Forms authentication configuration to use + /// Post request hook delegate + private static Action GetRedirectToLoginHook(CustomAuthenticationConfiguration configuration) + { + return context => + { + if (context.Response.StatusCode == HttpStatusCode.Unauthorized) + { + string redirectQuerystringKey = GetRedirectQuerystringKey(configuration); + + context.Response = context.GetRedirect( + string.Format("{0}?{1}={2}", + configuration.RedirectUrl, + redirectQuerystringKey, + context.ToFullPath("~" + context.Request.Path + HttpUtility.UrlEncode(context.Request.Url.Query)))); + } + }; + } + + /// + /// Gets the authenticated user GUID from the incoming request cookie if it exists + /// and is valid. + /// + /// Current context + /// Current configuration + /// Returns user guid, or Guid.Empty if not present or invalid + private static Guid GetAuthenticatedUserFromCookie(NancyContext context, CustomAuthenticationConfiguration configuration) + { + if (!context.Request.Cookies.ContainsKey(formsAuthenticationCookieName)) + { + return Guid.Empty; + } + + var cookieValueEncrypted = context.Request.Cookies[formsAuthenticationCookieName]; + + if (string.IsNullOrEmpty(cookieValueEncrypted)) + { + return Guid.Empty; + } + + var cookieValue = DecryptAndValidateAuthenticationCookie(cookieValueEncrypted, configuration); + + Guid returnGuid; + if (string.IsNullOrEmpty(cookieValue) || !Guid.TryParse(cookieValue, out returnGuid)) + { + return Guid.Empty; + } + + return returnGuid; + } + + /// + /// Build the forms authentication cookie + /// + /// Authenticated user identifier + /// Optional expiry date for the cookie (for 'Remember me') + /// Current configuration + /// Nancy cookie instance + private static INancyCookie BuildCookie(Guid userIdentifier, DateTime? cookieExpiry, CustomAuthenticationConfiguration configuration) + { + var cookieContents = EncryptAndSignCookie(userIdentifier.ToString(), configuration); + + var cookie = new NancyCookie(formsAuthenticationCookieName, cookieContents, true, configuration.RequiresSSL, cookieExpiry); + + if (!string.IsNullOrEmpty(configuration.Domain)) + { + cookie.Domain = configuration.Domain; + } + + if (!string.IsNullOrEmpty(configuration.Path)) + { + cookie.Path = configuration.Path; + } + + return cookie; + } + + /// + /// Builds a cookie for logging a user out + /// + /// Current configuration + /// Nancy cookie instance + private static INancyCookie BuildLogoutCookie(CustomAuthenticationConfiguration configuration) + { + var cookie = new NancyCookie(formsAuthenticationCookieName, String.Empty, true, configuration.RequiresSSL, DateTime.Now.AddDays(-1)); + + if (!string.IsNullOrEmpty(configuration.Domain)) + { + cookie.Domain = configuration.Domain; + } + + if (!string.IsNullOrEmpty(configuration.Path)) + { + cookie.Path = configuration.Path; + } + + return cookie; + } + + /// + /// Encrypt and sign the cookie contents + /// + /// Plain text cookie value + /// Current configuration + /// Encrypted and signed string + private static string EncryptAndSignCookie(string cookieValue, CustomAuthenticationConfiguration configuration) + { + var encryptedCookie = configuration.CryptographyConfiguration.EncryptionProvider.Encrypt(cookieValue); + var hmacBytes = GenerateHmac(encryptedCookie, configuration); + var hmacString = Convert.ToBase64String(hmacBytes); + + return String.Format("{1}{0}", encryptedCookie, hmacString); + } + + /// + /// Generate a hmac for the encrypted cookie string + /// + /// Encrypted cookie string + /// Current configuration + /// Hmac byte array + private static byte[] GenerateHmac(string encryptedCookie, CustomAuthenticationConfiguration configuration) + { + return configuration.CryptographyConfiguration.HmacProvider.GenerateHmac(encryptedCookie); + } + + /// + /// Decrypt and validate an encrypted and signed cookie value + /// + /// Encrypted and signed cookie value + /// Current configuration + /// Decrypted value, or empty on error or if failed validation + public static string DecryptAndValidateAuthenticationCookie(string cookieValue, CustomAuthenticationConfiguration configuration) + { + var hmacStringLength = Base64Helpers.GetBase64Length(configuration.CryptographyConfiguration.HmacProvider.HmacLength); + + var encryptedCookie = cookieValue.Substring(hmacStringLength); + var hmacString = cookieValue.Substring(0, hmacStringLength); + + var encryptionProvider = configuration.CryptographyConfiguration.EncryptionProvider; + + // Check the hmacs, but don't early exit if they don't match + var hmacBytes = Convert.FromBase64String(hmacString); + var newHmac = GenerateHmac(encryptedCookie, configuration); + var hmacValid = HmacComparer.Compare(newHmac, hmacBytes, configuration.CryptographyConfiguration.HmacProvider.HmacLength); + + var decrypted = encryptionProvider.Decrypt(encryptedCookie); + + // Only return the decrypted result if the hmac was ok + return hmacValid ? decrypted : string.Empty; + } + + /// + /// Gets the redirect query string key from + /// + /// The forms authentication configuration. + /// Redirect Querystring key + private static string GetRedirectQuerystringKey(CustomAuthenticationConfiguration configuration) + { + string redirectQuerystringKey = null; + + if (configuration != null) + { + redirectQuerystringKey = configuration.RedirectQuerystringKey; + } + + if (string.IsNullOrWhiteSpace(redirectQuerystringKey)) + { + redirectQuerystringKey = CustomAuthenticationConfiguration.DefaultRedirectQuerystringKey; + } + + return redirectQuerystringKey; + } + } +} diff --git a/PlexRequests.UI/Authentication/CustomModuleExtensions.cs b/PlexRequests.UI/Authentication/CustomModuleExtensions.cs new file mode 100644 index 000000000..5afac1c07 --- /dev/null +++ b/PlexRequests.UI/Authentication/CustomModuleExtensions.cs @@ -0,0 +1,108 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: CustomModuleExtensions.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; +using Nancy; +using Nancy.Authentication.Forms; +using Nancy.Extensions; + +namespace PlexRequests.UI.Authentication +{ + public static class CustomModuleExtensions + { + + /// + /// Logs the user in and returns either an empty 200 response for ajax requests, or a redirect response for non-ajax. + /// + /// Nancy module + /// User identifier guid + /// Optional expiry date for the cookie (for 'Remember me') + /// Url to redirect to if none in the querystring + /// Nancy response with redirect if request was not ajax, otherwise with OK. + public static Response Login(this INancyModule module, Guid userIdentifier, DateTime? cookieExpiry = null, string fallbackRedirectUrl = "/") + { + if (!module.Context.Request.IsAjaxRequest()) + return module.LoginAndRedirect(userIdentifier, cookieExpiry, fallbackRedirectUrl); + return module.LoginWithoutRedirect(userIdentifier, cookieExpiry); + } + + /// + /// Logs the user in with the given user guid and redirects. + /// + /// Nancy module + /// User identifier guid + /// Optional expiry date for the cookie (for 'Remember me') + /// Url to redirect to if none in the querystring + /// Nancy response instance + public static Response LoginAndRedirect(this INancyModule module, Guid userIdentifier, DateTime? cookieExpiry = null, string fallbackRedirectUrl = "/") + { + return CustomAuthenticationProvider.UserLoggedInRedirectResponse(module.Context, userIdentifier, cookieExpiry, fallbackRedirectUrl); + } + + /// + /// Logs the user in with the given user guid and returns ok response. + /// + /// Nancy module + /// User identifier guid + /// Optional expiry date for the cookie (for 'Remember me') + /// Nancy response instance + public static Response LoginWithoutRedirect(this INancyModule module, Guid userIdentifier, DateTime? cookieExpiry = null) + { + return CustomAuthenticationProvider.UserLoggedInResponse(userIdentifier, cookieExpiry); + } + + /// + /// Logs the user out and returns either an empty 200 response for ajax requests, or a redirect response for non-ajax. + /// + /// Nancy module + /// URL to redirect to + /// Nancy response with redirect if request was not ajax, otherwise with OK. + public static Response Logout(this INancyModule module, string redirectUrl) + { + if (!module.Context.Request.IsAjaxRequest()) + return CustomAuthenticationProvider.LogOutAndRedirectResponse(module.Context, redirectUrl); + return CustomAuthenticationProvider.LogOutResponse(); + } + + /// Logs the user out and redirects + /// Nancy module + /// URL to redirect to + /// Nancy response instance + public static Response LogoutAndRedirect(this INancyModule module, string redirectUrl) + { + return CustomAuthenticationProvider.LogOutAndRedirectResponse(module.Context, redirectUrl); + } + + /// Logs the user out without a redirect + /// Nancy module + /// Nancy response instance + public static Response LogoutWithoutRedirect(this INancyModule module) + { + return CustomAuthenticationProvider.LogOutResponse(); + } + } +} diff --git a/PlexRequests.UI/Bootstrapper.cs b/PlexRequests.UI/Bootstrapper.cs index f02f2f137..7dbb5765a 100644 --- a/PlexRequests.UI/Bootstrapper.cs +++ b/PlexRequests.UI/Bootstrapper.cs @@ -52,6 +52,7 @@ using PlexRequests.UI.Helpers; using Nancy.Json; using Ninject; +using PlexRequests.UI.Authentication; namespace PlexRequests.UI { @@ -92,13 +93,14 @@ namespace PlexRequests.UI var redirect = string.IsNullOrEmpty(baseUrl) ? "~/login" : $"~/{baseUrl}/login"; // Enable forms auth - var formsAuthConfiguration = new FormsAuthenticationConfiguration + var config = new CustomAuthenticationConfiguration { RedirectUrl = redirect, - UserMapper = container.Get() + PlexUserRepository = container.Get(), + LocalUserRepository = container.Get() }; - FormsAuthentication.Enable(pipelines, formsAuthConfiguration); + CustomAuthenticationProvider.Enable(pipelines, config); ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls; ServicePointManager.ServerCertificateValidationCallback += diff --git a/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs b/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs index ef8d93bc7..1a3d70205 100644 --- a/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs +++ b/PlexRequests.UI/Helpers/HtmlSecurityHelper.cs @@ -44,7 +44,8 @@ namespace PlexRequests.UI.Helpers { var userRepo = ServiceLocator.Instance.Resolve(); var linker = ServiceLocator.Instance.Resolve(); - return _security ?? (_security = new SecurityExtensions(userRepo, null, linker)); + var plex = ServiceLocator.Instance.Resolve(); + return _security ?? (_security = new SecurityExtensions(userRepo, null, linker, plex)); } } diff --git a/PlexRequests.UI/Helpers/ISecurityExtensions.cs b/PlexRequests.UI/Helpers/ISecurityExtensions.cs new file mode 100644 index 000000000..fb2a8ebf7 --- /dev/null +++ b/PlexRequests.UI/Helpers/ISecurityExtensions.cs @@ -0,0 +1,22 @@ +using System; +using Nancy; +using Nancy.Security; +using PlexRequests.Helpers.Permissions; + +namespace PlexRequests.UI.Helpers +{ + public interface ISecurityExtensions + { + Response AdminLoginRedirect(Permissions perm, NancyContext context); + bool DoesNotHavePermissions(Permissions perm, IUserIdentity currentUser); + bool DoesNotHavePermissions(int perm, IUserIdentity currentUser); + Func ForbiddenIfNot(Func test); + bool HasAnyPermissions(IUserIdentity user, params Permissions[] perm); + bool HasPermissions(IUserIdentity user, Permissions perm); + Response HasPermissionsRedirect(Permissions perm, NancyContext context, string routeName, HttpStatusCode code); + Func HttpStatusCodeIfNot(HttpStatusCode statusCode, Func test); + bool IsLoggedIn(NancyContext context); + bool IsNormalUser(NancyContext context); + bool IsPlexUser(NancyContext context); + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Helpers/SecurityExtensions.cs b/PlexRequests.UI/Helpers/SecurityExtensions.cs index a5dcf61de..d6ff46122 100644 --- a/PlexRequests.UI/Helpers/SecurityExtensions.cs +++ b/PlexRequests.UI/Helpers/SecurityExtensions.cs @@ -26,32 +26,30 @@ #endregion using System; -using System.Collections.Generic; -using System.Linq; using Nancy; -using Nancy.Extensions; using Nancy.Linker; using Nancy.Responses; using Nancy.Security; -using Ninject; using PlexRequests.Helpers.Permissions; using PlexRequests.Store.Repository; using PlexRequests.UI.Models; namespace PlexRequests.UI.Helpers { - public class SecurityExtensions + public class SecurityExtensions : ISecurityExtensions { - public SecurityExtensions(IUserRepository userRepository, NancyModule context, IResourceLinker linker) + public SecurityExtensions(IUserRepository userRepository, NancyModule context, IResourceLinker linker, IPlexUserRepository plexUsers) { UserRepository = userRepository; Module = context; Linker = linker; + PlexUsers = plexUsers; } private IUserRepository UserRepository { get; } private NancyModule Module { get; } private IResourceLinker Linker { get; } + private IPlexUserRepository PlexUsers { get; } public bool IsLoggedIn(NancyContext context) { @@ -99,11 +97,7 @@ namespace PlexRequests.UI.Helpers { return ForbiddenIfNot(ctx => { - var dbUser = UserRepository.GetUserByUsername(ctx.CurrentUser.UserName); - - if (dbUser == null) return false; - - var permissions = (Permissions)dbUser.Permissions; + var permissions = GetPermissions(ctx.CurrentUser); var result = permissions.HasFlag((Permissions)perm); return !result; }); @@ -116,37 +110,21 @@ namespace PlexRequests.UI.Helpers public bool DoesNotHavePermissions(Permissions perm, IUserIdentity currentUser) { - var dbUser = UserRepository.GetUserByUsername(currentUser.UserName); - - if (dbUser == null) return false; - - var permissions = (Permissions)dbUser.Permissions; + var permissions = GetPermissions(currentUser); var result = permissions.HasFlag(perm); return !result; } public bool HasPermissions(IUserIdentity user, Permissions perm) { - if (user == null) return false; - - var dbUser = UserRepository.GetUserByUsername(user.UserName); - - if (dbUser == null) return false; - - var permissions = (Permissions)dbUser.Permissions; - var result = permissions.HasFlag(perm); - return result; + var permissions = GetPermissions(user); + return permissions.HasFlag(perm); } public bool HasAnyPermissions(IUserIdentity user, params Permissions[] perm) { - if (user == null) return false; + var permissions = GetPermissions(user); - var dbUser = UserRepository.GetUserByUsername(user.UserName); - - if (dbUser == null) return false; - - var permissions = (Permissions)dbUser.Permissions; foreach (var p in perm) { var result = permissions.HasFlag(p); @@ -165,13 +143,7 @@ namespace PlexRequests.UI.Helpers var response = ForbiddenIfNot(ctx => { - if (ctx.CurrentUser == null) return false; - - var dbUser = UserRepository.GetUserByUsername(ctx.CurrentUser.UserName); - - if (dbUser == null) return false; - - var permissions = (Permissions) dbUser.Permissions; + var permissions = GetPermissions(ctx.CurrentUser); var result = permissions.HasFlag(perm); return result; }); @@ -228,5 +200,26 @@ namespace PlexRequests.UI.Helpers }; } + private Permissions GetPermissions(IUserIdentity user) + { + if (user == null) return 0; + + var dbUser = UserRepository.GetUserByUsername(user.UserName); + + if (dbUser != null) + { + var permissions = (Permissions)dbUser.Permissions; + return permissions; + } + + var plexUser = PlexUsers.GetUserByUsername(user.UserName); + if (plexUser != null) + { + var permissions = (Permissions)plexUser.Permissions; + return permissions; + } + + return 0; + } } } \ No newline at end of file diff --git a/PlexRequests.UI/Jobs/Scheduler.cs b/PlexRequests.UI/Jobs/Scheduler.cs index 7e180e907..06bdd7c6f 100644 --- a/PlexRequests.UI/Jobs/Scheduler.cs +++ b/PlexRequests.UI/Jobs/Scheduler.cs @@ -63,7 +63,8 @@ namespace PlexRequests.UI.Jobs { JobBuilder.Create().WithIdentity("PlexAvailabilityChecker", "Plex").Build(), JobBuilder.Create().WithIdentity("PlexContentCacher", "Plex").Build(), - JobBuilder.Create().WithIdentity("PlexEpisodeCacher", "Cache").Build(), + JobBuilder.Create().WithIdentity("PlexEpisodeCacher", "Plex").Build(), + JobBuilder.Create().WithIdentity("PlexUserChecker", "Plex").Build(), JobBuilder.Create().WithIdentity("SickRageCacher", "Cache").Build(), JobBuilder.Create().WithIdentity("SonarrCacher", "Cache").Build(), JobBuilder.Create().WithIdentity("CouchPotatoCacher", "Cache").Build(), @@ -159,6 +160,10 @@ namespace PlexRequests.UI.Jobs { s.PlexContentCacher = 60; } + if (s.PlexUserChecker == 0) + { + s.PlexUserChecker = 24; + } var triggers = new List(); @@ -175,6 +180,13 @@ namespace PlexRequests.UI.Jobs .WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexContentCacher).RepeatForever()) .Build(); + var plexUserChecker = + TriggerBuilder.Create() + .WithIdentity("PlexUserChecker", "Plex") + .StartNow() + .WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexUserChecker).RepeatForever()) + .Build(); + var srCacher = TriggerBuilder.Create() .WithIdentity("SickRageCacher", "Cache") @@ -253,6 +265,7 @@ namespace PlexRequests.UI.Jobs triggers.Add(plexEpCacher); triggers.Add(fault); triggers.Add(plexCacher); + triggers.Add(plexUserChecker); return triggers; } diff --git a/PlexRequests.UI/Modules/Admin/AdminModule.cs b/PlexRequests.UI/Modules/Admin/AdminModule.cs index f621d99cf..6f7f9d051 100644 --- a/PlexRequests.UI/Modules/Admin/AdminModule.cs +++ b/PlexRequests.UI/Modules/Admin/AdminModule.cs @@ -124,7 +124,8 @@ namespace PlexRequests.UI.Modules ICacheProvider cache, ISettingsService slackSettings, ISlackApi slackApi, ISettingsService lp, ISettingsService scheduler, IJobRecord rec, IAnalytics analytics, - ISettingsService notifyService, IRecentlyAdded recentlyAdded) : base("admin", prService) + ISettingsService notifyService, IRecentlyAdded recentlyAdded + , ISecurityExtensions security) : base("admin", prService, security) { PrService = prService; CpService = cpService; diff --git a/PlexRequests.UI/Modules/Admin/FaultQueueModule.cs b/PlexRequests.UI/Modules/Admin/FaultQueueModule.cs index b65238b23..1172be7af 100644 --- a/PlexRequests.UI/Modules/Admin/FaultQueueModule.cs +++ b/PlexRequests.UI/Modules/Admin/FaultQueueModule.cs @@ -35,13 +35,14 @@ using PlexRequests.Helpers.Permissions; using PlexRequests.Store; using PlexRequests.Store.Models; using PlexRequests.Store.Repository; +using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; namespace PlexRequests.UI.Modules.Admin { public class FaultQueueModule : BaseModule { - public FaultQueueModule(ISettingsService settingsService, ICacheProvider cache, IRepository requestQueue) : base("admin", settingsService) + public FaultQueueModule(ISettingsService settingsService, ICacheProvider cache, IRepository requestQueue, ISecurityExtensions security) : base("admin", settingsService, security) { Cache = cache; RequestQueue = requestQueue; diff --git a/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs b/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs index 7c5c793c5..ac9745b59 100644 --- a/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs +++ b/PlexRequests.UI/Modules/Admin/SystemStatusModule.cs @@ -38,13 +38,14 @@ using PlexRequests.Core.SettingModels; using PlexRequests.Core.StatusChecker; using PlexRequests.Helpers; using PlexRequests.Helpers.Permissions; +using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; namespace PlexRequests.UI.Modules.Admin { public class SystemStatusModule : BaseModule { - public SystemStatusModule(ISettingsService settingsService, ICacheProvider cache, ISettingsService ss) : base("admin", settingsService) + public SystemStatusModule(ISettingsService settingsService, ICacheProvider cache, ISettingsService ss, ISecurityExtensions security) : base("admin", settingsService, security) { Cache = cache; SystemSettings = ss; diff --git a/PlexRequests.UI/Modules/ApiDocsModule.cs b/PlexRequests.UI/Modules/ApiDocsModule.cs index 94dbaeda5..a92a0e26a 100644 --- a/PlexRequests.UI/Modules/ApiDocsModule.cs +++ b/PlexRequests.UI/Modules/ApiDocsModule.cs @@ -29,12 +29,13 @@ using Nancy.Responses.Negotiation; using PlexRequests.Core; using PlexRequests.Core.SettingModels; +using PlexRequests.UI.Helpers; namespace PlexRequests.UI.Modules { public class ApiDocsModule : BaseModule { - public ApiDocsModule(ISettingsService pr) : base("apidocs", pr) + public ApiDocsModule(ISettingsService pr, ISecurityExtensions security) : base("apidocs", pr, security) { Get["/"] = x => Documentation(); } diff --git a/PlexRequests.UI/Modules/ApiRequestModule.cs b/PlexRequests.UI/Modules/ApiRequestModule.cs index 6e56ce6ff..210ffa300 100644 --- a/PlexRequests.UI/Modules/ApiRequestModule.cs +++ b/PlexRequests.UI/Modules/ApiRequestModule.cs @@ -37,13 +37,14 @@ using Newtonsoft.Json; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Store; +using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; namespace PlexRequests.UI.Modules { public class ApiRequestModule : BaseApiModule { - public ApiRequestModule(IRequestService service, ISettingsService pr) : base("api", pr) + public ApiRequestModule(IRequestService service, ISettingsService pr, ISecurityExtensions security) : base("api", pr, security) { Get["GetRequests","/requests"] = x => GetRequests(); Get["GetRequest","/requests/{id}"] = x => GetSingleRequests(x); diff --git a/PlexRequests.UI/Modules/ApiSettingsModule.cs b/PlexRequests.UI/Modules/ApiSettingsModule.cs index 3ae55afec..bd8f700ca 100644 --- a/PlexRequests.UI/Modules/ApiSettingsModule.cs +++ b/PlexRequests.UI/Modules/ApiSettingsModule.cs @@ -37,6 +37,7 @@ using Newtonsoft.Json; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; +using PlexRequests.UI.Helpers; namespace PlexRequests.UI.Modules { @@ -44,7 +45,7 @@ namespace PlexRequests.UI.Modules { public ApiSettingsModule(ISettingsService pr, ISettingsService auth, ISettingsService plexSettings, ISettingsService cp, - ISettingsService sonarr, ISettingsService sr, ISettingsService hp) : base("api", pr) + ISettingsService sonarr, ISettingsService sr, ISettingsService hp, ISecurityExtensions security) : base("api", pr, security) { Get["GetVersion", "/version"] = x => GetVersion(); diff --git a/PlexRequests.UI/Modules/ApiUserModule.cs b/PlexRequests.UI/Modules/ApiUserModule.cs index 545558401..38a77b119 100644 --- a/PlexRequests.UI/Modules/ApiUserModule.cs +++ b/PlexRequests.UI/Modules/ApiUserModule.cs @@ -33,13 +33,14 @@ using Nancy.ModelBinding; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Store; +using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; namespace PlexRequests.UI.Modules { public class ApiUserModule : BaseApiModule { - public ApiUserModule(ISettingsService pr, ICustomUserMapper m) : base("api", pr) + public ApiUserModule(ISettingsService pr, ICustomUserMapper m, ISecurityExtensions security) : base("api", pr, security) { Put["PutCredentials", "/credentials/{username}"] = x => ChangePassword(x); diff --git a/PlexRequests.UI/Modules/ApplicationTesterModule.cs b/PlexRequests.UI/Modules/ApplicationTesterModule.cs index 6963fec9a..ec3fe6edd 100644 --- a/PlexRequests.UI/Modules/ApplicationTesterModule.cs +++ b/PlexRequests.UI/Modules/ApplicationTesterModule.cs @@ -47,7 +47,7 @@ namespace PlexRequests.UI.Modules { public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPlexApi plexApi, - ISickRageApi srApi, IHeadphonesApi hpApi, ISettingsService pr) : base("test", pr) + ISickRageApi srApi, IHeadphonesApi hpApi, ISettingsService pr, ISecurityExtensions security) : base("test", pr, security) { this.RequiresAuthentication(); diff --git a/PlexRequests.UI/Modules/ApprovalModule.cs b/PlexRequests.UI/Modules/ApprovalModule.cs index 8d3cf0c87..c4af80583 100644 --- a/PlexRequests.UI/Modules/ApprovalModule.cs +++ b/PlexRequests.UI/Modules/ApprovalModule.cs @@ -52,7 +52,8 @@ namespace PlexRequests.UI.Modules public ApprovalModule(IRequestService service, ISettingsService cpService, ICouchPotatoApi cpApi, ISonarrApi sonarrApi, ISettingsService sonarrSettings, ISickRageApi srApi, ISettingsService srSettings, - ISettingsService hpSettings, IHeadphonesApi hpApi, ISettingsService pr, ITransientFaultQueue faultQueue) : base("approval", pr) + ISettingsService hpSettings, IHeadphonesApi hpApi, ISettingsService pr, ITransientFaultQueue faultQueue + , ISecurityExtensions security) : base("approval", pr, security) { this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); @@ -68,6 +69,7 @@ namespace PlexRequests.UI.Modules SickRageSettings = srSettings; HeadphonesSettings = hpSettings; HeadphoneApi = hpApi; + FaultQueue = faultQueue; Post["/approve", true] = async (x, ct) => await Approve((int)Request.Form.requestid, (string)Request.Form.qualityId); Post["/deny", true] = async (x, ct) => await DenyRequest((int)Request.Form.requestid, (string)Request.Form.reason); diff --git a/PlexRequests.UI/Modules/BaseApiModule.cs b/PlexRequests.UI/Modules/BaseApiModule.cs index c7c1f4fc8..1652939fa 100644 --- a/PlexRequests.UI/Modules/BaseApiModule.cs +++ b/PlexRequests.UI/Modules/BaseApiModule.cs @@ -33,18 +33,19 @@ using Nancy.Validation; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Store; +using PlexRequests.UI.Helpers; namespace PlexRequests.UI.Modules { public abstract class BaseApiModule : BaseModule { - protected BaseApiModule(ISettingsService s) : base(s) + protected BaseApiModule(ISettingsService s, ISecurityExtensions security) : base(s,security) { Settings = s; Before += (ctx) => CheckAuth(); } - protected BaseApiModule(string modulePath, ISettingsService s) : base(modulePath, s) + protected BaseApiModule(string modulePath, ISettingsService s, ISecurityExtensions security) : base(modulePath, s, security) { Settings = s; Before += (ctx) => CheckAuth(); diff --git a/PlexRequests.UI/Modules/BaseAuthModule.cs b/PlexRequests.UI/Modules/BaseAuthModule.cs index e1a3a26de..b4cfb6436 100644 --- a/PlexRequests.UI/Modules/BaseAuthModule.cs +++ b/PlexRequests.UI/Modules/BaseAuthModule.cs @@ -31,18 +31,19 @@ using Nancy.Extensions; using PlexRequests.UI.Models; using PlexRequests.Core; using PlexRequests.Core.SettingModels; +using PlexRequests.UI.Helpers; namespace PlexRequests.UI.Modules { public abstract class BaseAuthModule : BaseModule { - protected BaseAuthModule(ISettingsService pr) : base(pr) + protected BaseAuthModule(ISettingsService pr, ISecurityExtensions security) : base(pr,security) { PlexRequestSettings = pr; Before += (ctx) => CheckAuth(); } - protected BaseAuthModule(string modulePath, ISettingsService pr) : base(modulePath, pr) + protected BaseAuthModule(string modulePath, ISettingsService pr, ISecurityExtensions security) : base(modulePath, pr, security) { PlexRequestSettings = pr; Before += (ctx) => CheckAuth(); diff --git a/PlexRequests.UI/Modules/BaseModule.cs b/PlexRequests.UI/Modules/BaseModule.cs index 7dc91ccca..4dfa98122 100644 --- a/PlexRequests.UI/Modules/BaseModule.cs +++ b/PlexRequests.UI/Modules/BaseModule.cs @@ -49,7 +49,7 @@ namespace PlexRequests.UI.Modules protected string BaseUrl { get; set; } - protected BaseModule(ISettingsService settingsService) + protected BaseModule(ISettingsService settingsService, ISecurityExtensions security) { var settings = settingsService.GetSettings(); @@ -59,11 +59,12 @@ namespace PlexRequests.UI.Modules var modulePath = string.IsNullOrEmpty(baseUrl) ? string.Empty : baseUrl; ModulePath = modulePath; + Security = security; Before += (ctx) => SetCookie(); } - protected BaseModule(string modulePath, ISettingsService settingsService) + protected BaseModule(string modulePath, ISettingsService settingsService, ISecurityExtensions security) { var settings = settingsService.GetSettings(); @@ -73,6 +74,7 @@ namespace PlexRequests.UI.Modules var settingModulePath = string.IsNullOrEmpty(baseUrl) ? modulePath : $"{baseUrl}/{modulePath}"; ModulePath = settingModulePath; + Security = security; Before += (ctx) => { @@ -100,8 +102,9 @@ namespace PlexRequests.UI.Modules return _dateTimeOffset; } } - private string _username; + + private string _username; protected string Username { get @@ -110,7 +113,7 @@ namespace PlexRequests.UI.Modules { try { - _username = Session[SessionKeys.UsernameKey].ToString(); + _username = User == null ? Session[SessionKeys.UsernameKey].ToString() : User.UserName; } catch (Exception) { @@ -131,33 +134,14 @@ namespace PlexRequests.UI.Modules { return false; } - - var userRepo = ServiceLocator.Instance.Resolve(); - - var user = userRepo.GetUserByUsername(Context?.CurrentUser?.UserName); - - if (user == null) return false; - - var permissions = (Permissions) user.Permissions; - return permissions.HasFlag(Permissions.Administrator); + + return Security.HasPermissions(Context?.CurrentUser, Permissions.Administrator); } } protected IUserIdentity User => Context?.CurrentUser; - protected SecurityExtensions Security - { - - get - { - var userRepo = ServiceLocator.Instance.Resolve(); - var linker = ServiceLocator.Instance.Resolve(); - return _security ?? (_security = new SecurityExtensions(userRepo, this, linker)); - } - } - - private SecurityExtensions _security; - + protected ISecurityExtensions Security { get; set; } protected bool LoggedIn => Context?.CurrentUser != null; diff --git a/PlexRequests.UI/Modules/CultureModule.cs b/PlexRequests.UI/Modules/CultureModule.cs index 8d093d32a..d4b57c8e0 100644 --- a/PlexRequests.UI/Modules/CultureModule.cs +++ b/PlexRequests.UI/Modules/CultureModule.cs @@ -41,7 +41,7 @@ namespace PlexRequests.UI.Modules { public class CultureModule : BaseModule { - public CultureModule(ISettingsService pr, IAnalytics a) : base("culture",pr) + public CultureModule(ISettingsService pr, IAnalytics a, ISecurityExtensions security) : base("culture",pr, security) { Analytics = a; diff --git a/PlexRequests.UI/Modules/DonationLinkModule.cs b/PlexRequests.UI/Modules/DonationLinkModule.cs index 5b3aec76a..4562528d0 100644 --- a/PlexRequests.UI/Modules/DonationLinkModule.cs +++ b/PlexRequests.UI/Modules/DonationLinkModule.cs @@ -7,12 +7,13 @@ using NLog; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; +using PlexRequests.UI.Helpers; namespace PlexRequests.UI.Modules { public class DonationLinkModule : BaseAuthModule { - public DonationLinkModule(ICacheProvider provider, ISettingsService pr) : base("customDonation", pr) + public DonationLinkModule(ICacheProvider provider, ISettingsService pr, ISecurityExtensions security) : base("customDonation", pr, security) { Cache = provider; diff --git a/PlexRequests.UI/Modules/IndexModule.cs b/PlexRequests.UI/Modules/IndexModule.cs index fd13acb35..9d11a64d3 100644 --- a/PlexRequests.UI/Modules/IndexModule.cs +++ b/PlexRequests.UI/Modules/IndexModule.cs @@ -33,12 +33,13 @@ using Nancy.Responses; using PlexRequests.Core; using PlexRequests.Core.SettingModels; +using PlexRequests.UI.Helpers; namespace PlexRequests.UI.Modules { public class IndexModule : BaseAuthModule { - public IndexModule(ISettingsService pr, ISettingsService l, IResourceLinker rl) : base(pr) + public IndexModule(ISettingsService pr, ISettingsService l, IResourceLinker rl, ISecurityExtensions security) : base(pr, security) { LandingPage = l; Linker = rl; diff --git a/PlexRequests.UI/Modules/IssuesModule.cs b/PlexRequests.UI/Modules/IssuesModule.cs index e3352c08a..9611b1bcf 100644 --- a/PlexRequests.UI/Modules/IssuesModule.cs +++ b/PlexRequests.UI/Modules/IssuesModule.cs @@ -26,7 +26,7 @@ namespace PlexRequests.UI.Modules { public class IssuesModule : BaseAuthModule { - public IssuesModule(ISettingsService pr, IIssueService issueService, IRequestService request, INotificationService n) : base("issues", pr) + public IssuesModule(ISettingsService pr, IIssueService issueService, IRequestService request, INotificationService n, ISecurityExtensions security) : base("issues", pr, security) { IssuesService = issueService; RequestService = request; diff --git a/PlexRequests.UI/Modules/LandingPageModule.cs b/PlexRequests.UI/Modules/LandingPageModule.cs index dccf69430..36199f598 100644 --- a/PlexRequests.UI/Modules/LandingPageModule.cs +++ b/PlexRequests.UI/Modules/LandingPageModule.cs @@ -33,6 +33,7 @@ using Nancy.Linker; using PlexRequests.Api.Interfaces; using PlexRequests.Core; using PlexRequests.Core.SettingModels; +using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; namespace PlexRequests.UI.Modules @@ -40,7 +41,7 @@ namespace PlexRequests.UI.Modules public class LandingPageModule : BaseModule { public LandingPageModule(ISettingsService settingsService, ISettingsService landing, - ISettingsService ps, IPlexApi pApi, IResourceLinker linker) : base("landing", settingsService) + ISettingsService ps, IPlexApi pApi, IResourceLinker linker, ISecurityExtensions security) : base("landing", settingsService, security) { LandingSettings = landing; PlexSettings = ps; diff --git a/PlexRequests.UI/Modules/LayoutModule.cs b/PlexRequests.UI/Modules/LayoutModule.cs index 3c1736dad..46e03fc26 100644 --- a/PlexRequests.UI/Modules/LayoutModule.cs +++ b/PlexRequests.UI/Modules/LayoutModule.cs @@ -38,13 +38,14 @@ using PlexRequests.Core.StatusChecker; using PlexRequests.Helpers; using PlexRequests.Services.Interfaces; using PlexRequests.Services.Jobs; +using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; namespace PlexRequests.UI.Modules { public class LayoutModule : BaseAuthModule { - public LayoutModule(ICacheProvider provider, ISettingsService pr, ISettingsService settings, IJobRecord rec) : base("layout", pr) + public LayoutModule(ICacheProvider provider, ISettingsService pr, ISettingsService settings, IJobRecord rec, ISecurityExtensions security) : base("layout", pr, security) { Cache = provider; SystemSettings = settings; diff --git a/PlexRequests.UI/Modules/LoginModule.cs b/PlexRequests.UI/Modules/LoginModule.cs index 9946a7de7..7c8c46f82 100644 --- a/PlexRequests.UI/Modules/LoginModule.cs +++ b/PlexRequests.UI/Modules/LoginModule.cs @@ -31,7 +31,6 @@ using System; using System.Dynamic; using System.Security; using Nancy; -using Nancy.Authentication.Forms; using Nancy.Extensions; using Nancy.Linker; using Nancy.Responses.Negotiation; @@ -43,14 +42,17 @@ using PlexRequests.Helpers; using PlexRequests.Helpers.Permissions; using PlexRequests.Store; using PlexRequests.Store.Repository; +using PlexRequests.UI.Authentication; +using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; +using ModuleExtensions = Nancy.Authentication.Forms.ModuleExtensions; namespace PlexRequests.UI.Modules { public class LoginModule : BaseModule { - public LoginModule(ISettingsService pr, ICustomUserMapper m, IResourceLinker linker, IRepository userLoginRepo) - : base(pr) + public LoginModule(ISettingsService pr, ICustomUserMapper m, IResourceLinker linker, IRepository userLoginRepo, ISecurityExtensions security) + : base(pr, security) { UserMapper = m; Get["LocalLogin","/login"] = _ => @@ -74,7 +76,7 @@ namespace PlexRequests.UI.Modules { Session.Delete(SessionKeys.UsernameKey); } - return this.LogoutAndRedirect(!string.IsNullOrEmpty(BaseUrl) ? $"~/{BaseUrl}/" : "~/"); + return CustomModuleExtensions.LogoutAndRedirect(this, !string.IsNullOrEmpty(BaseUrl) ? $"~/{BaseUrl}/" : "~/"); }; Post["/login"] = x => @@ -112,7 +114,7 @@ namespace PlexRequests.UI.Modules UserId = userId.ToString() }); - return this.LoginAndRedirect(userId.Value, expiry, redirect); + return CustomModuleExtensions.LoginAndRedirect(this,userId.Value, expiry, redirect); }; Get["/register"] = x => @@ -138,7 +140,7 @@ namespace PlexRequests.UI.Modules } var userId = UserMapper.CreateUser(username, Request.Form.Password, EnumHelper.All(), 0); Session[SessionKeys.UsernameKey] = username; - return this.LoginAndRedirect((Guid)userId); + return CustomModuleExtensions.LoginAndRedirect(this, (Guid)userId); }; Get["/changepassword"] = _ => ChangePassword(); diff --git a/PlexRequests.UI/Modules/RequestsBetaModule.cs b/PlexRequests.UI/Modules/RequestsBetaModule.cs deleted file mode 100644 index 067e10e62..000000000 --- a/PlexRequests.UI/Modules/RequestsBetaModule.cs +++ /dev/null @@ -1,453 +0,0 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: RequestsModule.cs -// Created By: Jamie Rees -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// ************************************************************************/ -#endregion - -using System; -using System.Linq; - -using Nancy; -using Nancy.Responses.Negotiation; -using Nancy.Security; - -using PlexRequests.Core; -using PlexRequests.Core.SettingModels; -using PlexRequests.Services.Interfaces; -using PlexRequests.Services.Notification; -using PlexRequests.Store; -using PlexRequests.UI.Models; -using PlexRequests.Helpers; -using PlexRequests.UI.Helpers; -using System.Collections.Generic; -using PlexRequests.Api.Interfaces; -using System.Threading.Tasks; - -using NLog; - -using PlexRequests.Core.Models; -using PlexRequests.Helpers.Analytics; - -using Action = PlexRequests.Helpers.Analytics.Action; - -namespace PlexRequests.UI.Modules -{ - public class RequestsBetaModule : BaseAuthModule - { - public RequestsBetaModule( - IRequestService service, - ISettingsService prSettings, - ISettingsService requestSettings, - ISettingsService plex, - INotificationService notify, - ISettingsService sonarrSettings, - ISettingsService sickRageSettings, - ISettingsService cpSettings, - ICouchPotatoApi cpApi, - ISonarrApi sonarrApi, - ISickRageApi sickRageApi, - ICacheProvider cache, - IAnalytics an) : base("requestsbeta", prSettings) - { - Service = service; - PrSettings = prSettings; - PlexSettings = plex; - NotificationService = notify; - SonarrSettings = sonarrSettings; - SickRageSettings = sickRageSettings; - CpSettings = cpSettings; - SonarrApi = sonarrApi; - SickRageApi = sickRageApi; - CpApi = cpApi; - Cache = cache; - Analytics = an; - - Get["/"] = x => LoadRequests(); - Get["/plexrequestsettings", true] = async (x, ct) => await GetPlexRequestSettings(); - Get["/requestsettings", true] = async (x, ct) => await GetRequestSettings(); - Get["/movies", true] = async (x, ct) => await GetMovies(); - Get["/movies/{searchTerm}", true] = async (x, ct) => await GetMovies((string)x.searchTerm); - - - // Everything below is not being used in the beta page - Get["/tvshows", true] = async (c, ct) => await GetTvShows(); - Get["/albums", true] = async (x, ct) => await GetAlbumRequests(); - Post["/delete", true] = async (x, ct) => await DeleteRequest((int)Request.Form.id); - Post["/reportissue", true] = async (x, ct) => await ReportIssue((int)Request.Form.requestId, (IssueState)(int)Request.Form.issue, null); - Post["/reportissuecomment", true] = async (x, ct) => await ReportIssue((int)Request.Form.requestId, IssueState.Other, (string)Request.Form.commentArea); - - Post["/clearissues", true] = async (x, ct) => await ClearIssue((int)Request.Form.Id); - - Post["/changeavailability", true] = async (x, ct) => await ChangeRequestAvailability((int)Request.Form.Id, (bool)Request.Form.Available); - } - - private static Logger Log = LogManager.GetCurrentClassLogger(); - private IRequestService Service { get; } - private IAnalytics Analytics { get; } - private INotificationService NotificationService { get; } - private ISettingsService PrSettings { get; } - private ISettingsService PlexSettings { get; } - private ISettingsService RequestSettings { get; } - private ISettingsService SonarrSettings { get; } - private ISettingsService SickRageSettings { get; } - private ISettingsService CpSettings { get; } - private ISonarrApi SonarrApi { get; } - private ISickRageApi SickRageApi { get; } - private ICouchPotatoApi CpApi { get; } - private ICacheProvider Cache { get; } - - private Negotiator LoadRequests() - { - return View["Index"]; - } - - private async Task GetPlexRequestSettings() - { - return Response.AsJson(await PrSettings.GetSettingsAsync()); - } - - private async Task GetRequestSettings() - { - return Response.AsJson(await RequestSettings.GetSettingsAsync()); - } - - private async Task GetMovies(string searchTerm = null, bool approved = false, bool notApproved = false, - bool available = false, bool notAvailable = false, bool released = false, bool notReleased = false) - { - var dbMovies = await FilterMovies(searchTerm, approved, notApproved, available, notAvailable, released, notReleased); - var qualities = await GetQualityProfiles(); - var model = MapMoviesToView(dbMovies.ToList(), qualities); - - return Response.AsJson(model); - } - - private async Task GetTvShows() - { - var settingsTask = PrSettings.GetSettingsAsync(); - - var requests = await Service.GetAllAsync(); - requests = requests.Where(x => x.Type == RequestType.TvShow); - - var dbTv = requests; - var settings = await settingsTask; - if (settings.UsersCanViewOnlyOwnRequests && !IsAdmin) - { - dbTv = dbTv.Where(x => x.UserHasRequested(Username)).ToList(); - } - - IEnumerable qualities = new List(); - if (IsAdmin) - { - try - { - var sonarrSettings = await SonarrSettings.GetSettingsAsync(); - if (sonarrSettings.Enabled) - { - var result = Cache.GetOrSetAsync(CacheKeys.SonarrQualityProfiles, async () => - { - return await Task.Run(() => SonarrApi.GetProfiles(sonarrSettings.ApiKey, sonarrSettings.FullUri)); - }); - qualities = result.Result.Select(x => new QualityModel { Id = x.id.ToString(), Name = x.name }).ToList(); - } - else - { - var sickRageSettings = await SickRageSettings.GetSettingsAsync(); - if (sickRageSettings.Enabled) - { - qualities = sickRageSettings.Qualities.Select(x => new QualityModel { Id = x.Key, Name = x.Value }).ToList(); - } - } - } - catch (Exception e) - { - Log.Info(e); - } - - } - - var viewModel = dbTv.Select(tv => new RequestViewModel - { - ProviderId = tv.ProviderId, - Type = tv.Type, - Status = tv.Status, - ImdbId = tv.ImdbId, - Id = tv.Id, - PosterPath = tv.PosterPath, - ReleaseDate = tv.ReleaseDate, - ReleaseDateTicks = tv.ReleaseDate.Ticks, - RequestedDate = tv.RequestedDate, - RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(tv.RequestedDate, DateTimeOffset).Ticks, - Released = DateTime.Now > tv.ReleaseDate, - Approved = tv.Available || tv.Approved, - Title = tv.Title, - Overview = tv.Overview, - RequestedUsers = IsAdmin ? tv.AllUsers.ToArray() : new string[] { }, - ReleaseYear = tv.ReleaseDate.Year.ToString(), - Available = tv.Available, - Admin = IsAdmin, - IssueId = tv.IssueId, - TvSeriesRequestType = tv.SeasonsRequested, - Qualities = qualities.ToArray(), - Episodes = tv.Episodes.ToArray(), - }).ToList(); - - return Response.AsJson(viewModel); - } - - private async Task GetAlbumRequests() - { - var settings = PrSettings.GetSettings(); - var dbAlbum = await Service.GetAllAsync(); - dbAlbum = dbAlbum.Where(x => x.Type == RequestType.Album); - if (settings.UsersCanViewOnlyOwnRequests && !IsAdmin) - { - dbAlbum = dbAlbum.Where(x => x.UserHasRequested(Username)); - } - - var viewModel = dbAlbum.Select(album => - { - return new RequestViewModel - { - ProviderId = album.ProviderId, - Type = album.Type, - Status = album.Status, - ImdbId = album.ImdbId, - Id = album.Id, - PosterPath = album.PosterPath, - ReleaseDate = album.ReleaseDate, - ReleaseDateTicks = album.ReleaseDate.Ticks, - RequestedDate = album.RequestedDate, - RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(album.RequestedDate, DateTimeOffset).Ticks, - Released = DateTime.Now > album.ReleaseDate, - Approved = album.Available || album.Approved, - Title = album.Title, - Overview = album.Overview, - RequestedUsers = IsAdmin ? album.AllUsers.ToArray() : new string[] { }, - ReleaseYear = album.ReleaseDate.Year.ToString(), - Available = album.Available, - Admin = IsAdmin, - IssueId = album.IssueId, - TvSeriesRequestType = album.SeasonsRequested, - MusicBrainzId = album.MusicBrainzId, - ArtistName = album.ArtistName - - }; - }).ToList(); - - return Response.AsJson(viewModel); - } - - private async Task DeleteRequest(int requestid) - { - this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); - Analytics.TrackEventAsync(Category.Requests, Action.Delete, "Delete Request", Username, CookieHelper.GetAnalyticClientId(Cookies)); - - var currentEntity = await Service.GetAsync(requestid); - await Service.DeleteRequestAsync(currentEntity); - return Response.AsJson(new JsonResponseModel { Result = true }); - } - - /// - /// Reports the issue. - /// Comment can be null if the IssueState != Other - /// - /// The request identifier. - /// The issue. - /// The comment. - /// - private async Task ReportIssue(int requestId, IssueState issue, string comment) - { - var originalRequest = await Service.GetAsync(requestId); - if (originalRequest == null) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = "Could not add issue, please try again or contact the administrator!" }); - } - originalRequest.Issues = issue; - originalRequest.OtherMessage = !string.IsNullOrEmpty(comment) - ? $"{Username} - {comment}" - : string.Empty; - - - var result = await Service.UpdateRequestAsync(originalRequest); - - var model = new NotificationModel - { - User = Username, - NotificationType = NotificationType.Issue, - Title = originalRequest.Title, - DateTime = DateTime.Now, - Body = issue == IssueState.Other ? comment : issue.ToString().ToCamelCaseWords() - }; - await NotificationService.Publish(model); - - return Response.AsJson(result - ? new JsonResponseModel { Result = true } - : new JsonResponseModel { Result = false, Message = "Could not add issue, please try again or contact the administrator!" }); - } - - private async Task ClearIssue(int requestId) - { - this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); - - var originalRequest = await Service.GetAsync(requestId); - if (originalRequest == null) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = "Request does not exist to clear it!" }); - } - originalRequest.Issues = IssueState.None; - originalRequest.OtherMessage = string.Empty; - - var result = await Service.UpdateRequestAsync(originalRequest); - return Response.AsJson(result - ? new JsonResponseModel { Result = true } - : new JsonResponseModel { Result = false, Message = "Could not clear issue, please try again or check the logs" }); - } - - private async Task ChangeRequestAvailability(int requestId, bool available) - { - this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser); - Analytics.TrackEventAsync(Category.Requests, Action.Update, available ? "Make request available" : "Make request unavailable", Username, CookieHelper.GetAnalyticClientId(Cookies)); - var originalRequest = await Service.GetAsync(requestId); - if (originalRequest == null) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = "Request does not exist to change the availability!" }); - } - - originalRequest.Available = available; - - var result = await Service.UpdateRequestAsync(originalRequest); - return Response.AsJson(result - ? new { Result = true, Available = available, Message = string.Empty } - : new { Result = false, Available = false, Message = "Could not update the availability, please try again or check the logs" }); - } - - private List MapMoviesToView(List dbMovies, List qualities) - { - return dbMovies.Select(movie => new RequestViewModel - { - ProviderId = movie.ProviderId, - Type = movie.Type, - Status = movie.Status, - ImdbId = movie.ImdbId, - Id = movie.Id, - PosterPath = movie.PosterPath, - ReleaseDate = movie.ReleaseDate, - ReleaseDateTicks = movie.ReleaseDate.Ticks, - RequestedDate = movie.RequestedDate, - Released = DateTime.Now > movie.ReleaseDate, - RequestedDateTicks = DateTimeHelper.OffsetUTCDateTime(movie.RequestedDate, DateTimeOffset).Ticks, - Approved = movie.Available || movie.Approved, - Title = movie.Title, - Overview = movie.Overview, - RequestedUsers = IsAdmin ? movie.AllUsers.ToArray() : new string[] { }, - ReleaseYear = movie.ReleaseDate.Year.ToString(), - Available = movie.Available, - Admin = IsAdmin, - IssueId = movie.IssueId, - Qualities = qualities.ToArray() - }).ToList(); - } - - private async Task> GetQualityProfiles() - { - var qualities = new List(); - if (IsAdmin) - { - var cpSettings = CpSettings.GetSettings(); - if (cpSettings.Enabled) - { - try - { - var result = await Cache.GetOrSetAsync(CacheKeys.CouchPotatoQualityProfiles, async () => - { - return await Task.Run(() => CpApi.GetProfiles(cpSettings.FullUri, cpSettings.ApiKey)).ConfigureAwait(false); - }); - if (result != null) - { - qualities = result.list.Select(x => new QualityModel { Id = x._id, Name = x.label }).ToList(); - } - } - catch (Exception e) - { - Log.Info(e); - } - } - } - return qualities; - } - - private async Task> FilterMovies(string searchTerm = null, bool approved = false, bool notApproved = false, - bool available = false, bool notAvailable = false, bool released = false, bool notReleased = false) - { - var settings = PrSettings.GetSettings(); - var allRequests = await Service.GetAllAsync(); - allRequests = allRequests.Where(x => x.Type == RequestType.Movie); - - var dbMovies = allRequests; - - if (settings.UsersCanViewOnlyOwnRequests && !IsAdmin) - { - dbMovies = dbMovies.Where(x => x.UserHasRequested(Username)); - } - - // Filter the movies on the search term - if (!string.IsNullOrEmpty(searchTerm)) - { - dbMovies = dbMovies.Where(x => x.Title.Contains(searchTerm)); - } - - if (approved) - { - dbMovies = dbMovies.Where(x => x.Approved); - } - - if (notApproved) - { - dbMovies = dbMovies.Where(x => !x.Approved); - } - - if (available) - { - dbMovies = dbMovies.Where(x => x.Available); - } - - if (notAvailable) - { - dbMovies = dbMovies.Where(x => !x.Available); - } - - if (released) - { - dbMovies = dbMovies.Where(x => DateTime.Now > x.ReleaseDate); - } - - if (notReleased) - { - dbMovies = dbMovies.Where(x => DateTime.Now < x.ReleaseDate); - } - - return dbMovies; - } - } -} diff --git a/PlexRequests.UI/Modules/RequestsModule.cs b/PlexRequests.UI/Modules/RequestsModule.cs index 1b95841e6..006efe026 100644 --- a/PlexRequests.UI/Modules/RequestsModule.cs +++ b/PlexRequests.UI/Modules/RequestsModule.cs @@ -48,6 +48,7 @@ using NLog; using PlexRequests.Core.Models; using PlexRequests.Helpers.Analytics; using PlexRequests.Helpers.Permissions; +using PlexRequests.UI.Helpers; using Action = PlexRequests.Helpers.Analytics.Action; namespace PlexRequests.UI.Modules @@ -67,7 +68,8 @@ namespace PlexRequests.UI.Modules ISickRageApi sickRageApi, ICacheProvider cache, IAnalytics an, - INotificationEngine engine) : base("requests", prSettings) + INotificationEngine engine, + ISecurityExtensions security) : base("requests", prSettings, security) { Service = service; PrSettings = prSettings; diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 5c3792dae..53c4016b2 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -82,8 +82,8 @@ namespace PlexRequests.UI.Modules ICouchPotatoCacher cpCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi, ISettingsService plexService, ISettingsService auth, IRepository u, ISettingsService email, - IIssueService issue, IAnalytics a, IRepository rl, ITransientFaultQueue tfQueue, IRepository content) - : base("search", prSettings) + IIssueService issue, IAnalytics a, IRepository rl, ITransientFaultQueue tfQueue, IRepository content, ISecurityExtensions security) + : base("search", prSettings, security) { Auth = auth; PlexService = plexService; diff --git a/PlexRequests.UI/Modules/UserLoginModule.cs b/PlexRequests.UI/Modules/UserLoginModule.cs index f377cf2f3..760d4b267 100644 --- a/PlexRequests.UI/Modules/UserLoginModule.cs +++ b/PlexRequests.UI/Modules/UserLoginModule.cs @@ -44,17 +44,19 @@ using PlexRequests.Helpers; using PlexRequests.Helpers.Analytics; using PlexRequests.Store; using PlexRequests.Store.Repository; +using PlexRequests.UI.Authentication; +using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; +using ModuleExtensions = Nancy.Authentication.Forms.ModuleExtensions; -using Action = PlexRequests.Helpers.Analytics.Action; - namespace PlexRequests.UI.Modules { public class UserLoginModule : BaseModule { public UserLoginModule(ISettingsService auth, IPlexApi api, ISettingsService plexSettings, ISettingsService pr, - ISettingsService lp, IAnalytics a, IResourceLinker linker, IRepository userLogins) : base("userlogin", pr) + ISettingsService lp, IAnalytics a, IResourceLinker linker, IRepository userLogins, IPlexUserRepository plexUsers, ICustomUserMapper custom, ISecurityExtensions security) + : base("userlogin", pr, security) { AuthService = auth; LandingPageSettings = lp; @@ -63,6 +65,8 @@ namespace PlexRequests.UI.Modules PlexSettings = plexSettings; Linker = linker; UserLogins = userLogins; + PlexUserRepository = plexUsers; + CustomUserMapper = custom; Get["UserLoginIndex", "/", true] = async (x, ct) => { @@ -86,12 +90,15 @@ namespace PlexRequests.UI.Modules private IResourceLinker Linker { get; } private IAnalytics Analytics { get; } private IRepository UserLogins { get; } + private IPlexUserRepository PlexUserRepository { get; } + private ICustomUserMapper CustomUserMapper { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); private async Task LoginUser() { var userId = string.Empty; + var loginGuid = Guid.Empty; var dateTimeOffset = Request.Form.DateTimeOffset; var username = Request.Form.username.Value; Log.Debug("Username \"{0}\" attempting to login", username); @@ -122,6 +129,9 @@ namespace PlexRequests.UI.Modules password = Request.Form.password.Value; } + var localUsers = await CustomUserMapper.GetUsersAsync(); + var plexLocalUsers = await PlexUserRepository.GetAllAsync(); + if (settings.UserAuthentication && settings.UsePassword) // Authenticate with Plex { @@ -172,6 +182,18 @@ namespace PlexRequests.UI.Modules // Add to the session (Used in the BaseModules) Session[SessionKeys.UsernameKey] = (string)username; Session[SessionKeys.ClientDateTimeOffsetKey] = (int)dateTimeOffset; + + var plexLocal = plexLocalUsers.FirstOrDefault(x => x.Username == username); + if (plexLocal != null) + { + loginGuid = Guid.Parse(plexLocal.LoginId); + } + + var dbUser = localUsers.FirstOrDefault(x => x.UserName == username); + if (dbUser != null) + { + loginGuid = Guid.Parse(dbUser.UserGuid); + } } if (!authenticated) @@ -188,10 +210,20 @@ namespace PlexRequests.UI.Modules if (!landingSettings.BeforeLogin) { var uri = Linker.BuildRelativeUri(Context, "LandingPageIndex"); + if (loginGuid != Guid.Empty) + { + return CustomModuleExtensions.LoginAndRedirect(this, loginGuid, null, uri.ToString()); + } return Response.AsRedirect(uri.ToString()); } } + + var retVal = Linker.BuildRelativeUri(Context, "SearchIndex"); + if (loginGuid != Guid.Empty) + { + return CustomModuleExtensions.LoginAndRedirect(this, loginGuid, null, retVal.ToString()); + } return Response.AsRedirect(retVal.ToString()); } diff --git a/PlexRequests.UI/Modules/UserManagementModule.cs b/PlexRequests.UI/Modules/UserManagementModule.cs index 0bfd73d65..bae4b5740 100644 --- a/PlexRequests.UI/Modules/UserManagementModule.cs +++ b/PlexRequests.UI/Modules/UserManagementModule.cs @@ -17,13 +17,15 @@ using PlexRequests.Helpers.Permissions; using PlexRequests.Store; using PlexRequests.Store.Models; using PlexRequests.Store.Repository; +using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; namespace PlexRequests.UI.Modules { public class UserManagementModule : BaseModule { - public UserManagementModule(ISettingsService pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService plex, IRepository userLogins, IRepository plexRepo) : base("usermanagement", pr) + public UserManagementModule(ISettingsService pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService plex, IRepository userLogins, IPlexUserRepository plexRepo + , ISecurityExtensions security) : base("usermanagement", pr, security) { #if !DEBUG Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx); @@ -51,7 +53,7 @@ namespace PlexRequests.UI.Modules private IPlexApi PlexApi { get; } private ISettingsService PlexSettings { get; } private IRepository UserLoginsRepo { get; } - private IRepository PlexUsersRepository { get; } + private IPlexUserRepository PlexUsersRepository { get; } private ISettingsService PlexRequestSettings { get; } private Negotiator Load() @@ -112,11 +114,21 @@ namespace PlexRequests.UI.Modules { return Response.AsJson(new JsonResponseModel { - Result = true, + Result = false, Message = "Please enter in a valid Username and Password" }); } + var users = UserMapper.GetUsers(); + if (users.Any(x => x.UserName.Equals(model.Username, StringComparison.CurrentCultureIgnoreCase))) + { + return Response.AsJson(new JsonResponseModel + { + Result = false, + Message = $"A user with the username '{model.Username}' already exists" + }); + } + var featuresVal = 0; var permissionsVal = 0; @@ -213,7 +225,10 @@ namespace PlexRequests.UI.Modules Permissions = permissionsValue, Features = featuresValue, UserAlias = model.Alias, - PlexUserId = plexUser.Id + PlexUserId = plexUser.Id, + EmailAddress = plexUser.Email, + Username = plexUser.Username, + LoginId = Guid.NewGuid().ToString() }; await PlexUsersRepository.InsertAsync(user); diff --git a/PlexRequests.UI/Modules/UserWizardModule.cs b/PlexRequests.UI/Modules/UserWizardModule.cs index 2db86f7e5..b05b190ab 100644 --- a/PlexRequests.UI/Modules/UserWizardModule.cs +++ b/PlexRequests.UI/Modules/UserWizardModule.cs @@ -41,6 +41,7 @@ using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Helpers.Analytics; using PlexRequests.Helpers.Permissions; +using PlexRequests.UI.Authentication; using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; @@ -51,7 +52,7 @@ namespace PlexRequests.UI.Modules public class UserWizardModule : BaseModule { public UserWizardModule(ISettingsService pr, ISettingsService plex, IPlexApi plexApi, - ISettingsService auth, ICustomUserMapper m, IAnalytics a) : base("wizard", pr) + ISettingsService auth, ICustomUserMapper m, IAnalytics a, ISecurityExtensions security) : base("wizard", pr, security) { PlexSettings = plex; PlexApi = plexApi; @@ -200,7 +201,7 @@ namespace PlexRequests.UI.Modules var baseUrl = string.IsNullOrEmpty(settings.BaseUrl) ? string.Empty : $"/{settings.BaseUrl}"; - return this.LoginAndRedirect((Guid)userId, fallbackRedirectUrl: $"{baseUrl}/search"); + return CustomModuleExtensions.LoginAndRedirect(this,(Guid)userId, fallbackRedirectUrl: $"{baseUrl}/search"); } } } \ No newline at end of file diff --git a/PlexRequests.UI/NinjectModules/ConfigurationModule.cs b/PlexRequests.UI/NinjectModules/ConfigurationModule.cs index 0ed22372a..5efb59832 100644 --- a/PlexRequests.UI/NinjectModules/ConfigurationModule.cs +++ b/PlexRequests.UI/NinjectModules/ConfigurationModule.cs @@ -38,6 +38,7 @@ using PlexRequests.Helpers; using PlexRequests.Services.Interfaces; using PlexRequests.Services.Notification; using PlexRequests.Store; +using PlexRequests.UI.Helpers; namespace PlexRequests.UI.NinjectModules { @@ -59,6 +60,8 @@ namespace PlexRequests.UI.NinjectModules Bind().To(); Bind().To(); + + Bind().To(); } } } \ No newline at end of file diff --git a/PlexRequests.UI/NinjectModules/RepositoryModule.cs b/PlexRequests.UI/NinjectModules/RepositoryModule.cs index 66b7f6695..827453601 100644 --- a/PlexRequests.UI/NinjectModules/RepositoryModule.cs +++ b/PlexRequests.UI/NinjectModules/RepositoryModule.cs @@ -48,6 +48,7 @@ namespace PlexRequests.UI.NinjectModules Bind().To(); Bind().To(); + Bind().To(); } } diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 04f4a0cc6..2b4845cb0 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -203,6 +203,9 @@ + + + @@ -212,6 +215,7 @@ + @@ -257,7 +261,6 @@ - diff --git a/PlexRequests.UI/Views/Admin/SchedulerSettings.cshtml b/PlexRequests.UI/Views/Admin/SchedulerSettings.cshtml index 2173ecf77..27550f2b7 100644 --- a/PlexRequests.UI/Views/Admin/SchedulerSettings.cshtml +++ b/PlexRequests.UI/Views/Admin/SchedulerSettings.cshtml @@ -32,6 +32,16 @@ +
+ + +
+ +
+ + +
+