Fixed the issue with user management, needed to implement our own authentication provider

pull/733/head
Jamie.Rees 8 years ago
parent 63c2744336
commit 2a8927eb6d

@ -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<LogSettings> log, IPlexApi plexApi, ISettingsService<PlexSettings> plexService, IRepository<PlexUsers> plexusers,
ISettingsService<PlexRequestSettings> prSettings, ISettingsService<UserManagementSettings> umSettings)
public Version1100(IUserRepository userRepo, IRequestService requestService, ISettingsService<LogSettings> log, IPlexApi plexApi, ISettingsService<PlexSettings> plexService, IPlexUserRepository plexusers,
ISettingsService<PlexRequestSettings> prSettings, ISettingsService<UserManagementSettings> umSettings, ISettingsService<ScheduledJobsSettings> 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<LogSettings> Log { get; }
private IPlexApi PlexApi { get; }
private ISettingsService<PlexSettings> PlexSettings { get; }
private IRepository<PlexUsers> PlexUsers { get; }
private IPlexUserRepository PlexUsers { get; }
private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; }
private ISettingsService<UserManagementSettings> UserManagementSettings { get; }
private ISettingsService<ScheduledJobsSettings> 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

@ -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; }
}
}

@ -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";
}
}

@ -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> plexSettings, ISettingsService<PlexRequestSettings> 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> PlexSettings { get; }
private ISettingsService<PlexRequestSettings> 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);
}
}
}
}

@ -93,6 +93,7 @@
<Compile Include="Jobs\PlexEpisodeCacher.cs" />
<Compile Include="Jobs\RecentlyAdded.cs" />
<Compile Include="Jobs\StoreBackup.cs" />
<Compile Include="Jobs\PlexUserChecker.cs" />
<Compile Include="Jobs\StoreCleanup.cs" />
<Compile Include="Jobs\CouchPotatoCacher.cs" />
<Compile Include="Jobs\PlexAvailabilityChecker.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; }
}
}

@ -88,6 +88,7 @@
<Compile Include="Repository\SettingsJsonRepository.cs" />
<Compile Include="Repository\RequestJsonRepository.cs" />
<Compile Include="Repository\GenericRepository.cs" />
<Compile Include="Repository\PlexUserRepository.cs" />
<Compile Include="Repository\UserRepository.cs" />
<Compile Include="RequestedModel.cs" />
<Compile Include="UserEntity.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<PlexUsers>, 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<PlexUsers>(sql, new {UserGuid = userGuid});
}
public PlexUsers GetUserByUsername(string username)
{
var sql = @"SELECT * FROM PlexUsers
WHERE Username = @UserName";
return Db.QueryFirstOrDefault<PlexUsers>(sql, new {UserName = username});
}
public async Task<PlexUsers> GetUserAsync(string userguid)
{
var sql = @"SELECT * FROM PlexUsers
WHERE PlexUserId = @UserGuid";
return await Db.QueryFirstOrDefaultAsync<PlexUsers>(sql, new {UserGuid = userguid});
}
#region abstract implimentation
[Obsolete]
public override PlexUsers Get(string id)
{
throw new System.NotImplementedException();
}
[Obsolete]
public override Task<PlexUsers> GetAsync(int id)
{
throw new System.NotImplementedException();
}
[Obsolete]
public override PlexUsers Get(int id)
{
throw new System.NotImplementedException();
}
[Obsolete]
public override Task<PlexUsers> GetAsync(string id)
{
throw new System.NotImplementedException();
}
#endregion
}
public interface IPlexUserRepository
{
PlexUsers GetUser(string userGuid);
PlexUsers GetUserByUsername(string username);
Task<PlexUsers> GetUserAsync(string userguid);
IEnumerable<PlexUsers> Custom(Func<IDbConnection, IEnumerable<PlexUsers>> func);
long Insert(PlexUsers entity);
void Delete(PlexUsers entity);
IEnumerable<PlexUsers> GetAll();
bool UpdateAll(IEnumerable<PlexUsers> entity);
bool Update(PlexUsers entity);
Task<IEnumerable<PlexUsers>> GetAllAsync();
Task<bool> UpdateAsync(PlexUsers users);
Task<int> InsertAsync(PlexUsers users);
}
}

@ -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);

@ -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";
/// <summary>
/// Gets or sets the forms authentication query string key for storing the return url
/// </summary>
public string RedirectQuerystringKey { get; set; }
/// <summary>
/// Gets or sets the redirect url for pages that require authentication
/// </summary>
public string RedirectUrl { get; set; }
/// <summary>Gets or sets the username/identifier mapper</summary>
public IUserRepository LocalUserRepository { get; set; }
public IPlexUserRepository PlexUserRepository { get; set; }
/// <summary>Gets or sets RequiresSSL property</summary>
/// <value>The flag that indicates whether SSL is required</value>
public bool RequiresSSL { get; set; }
/// <summary>
/// Gets or sets whether to redirect to login page during unauthorized access.
/// </summary>
public bool DisableRedirect { get; set; }
/// <summary>Gets or sets the domain of the auth cookie</summary>
public string Domain { get; set; }
/// <summary>Gets or sets the path of the auth cookie</summary>
public string Path { get; set; }
/// <summary>Gets or sets the cryptography configuration</summary>
public CryptographyConfiguration CryptographyConfiguration { get; set; }
/// <summary>
/// Gets a value indicating whether the configuration is valid or not.
/// </summary>
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);
/// <summary>
/// Initializes a new instance of the <see cref="T:Nancy.Authentication.Forms.FormsAuthenticationConfiguration" /> class.
/// </summary>
public CustomAuthenticationConfiguration()
: this(CryptographyConfiguration.Default)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="T:Nancy.Authentication.Forms.FormsAuthenticationConfiguration" /> class.
/// </summary>
/// <param name="cryptographyConfiguration">Cryptography configuration</param>
public CustomAuthenticationConfiguration(CryptographyConfiguration cryptographyConfiguration)
{
this.CryptographyConfiguration = cryptographyConfiguration;
this.RedirectQuerystringKey = "returnUrl";
}
}
}

@ -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;
/// <summary>Gets or sets the forms authentication cookie name</summary>
public static string FormsAuthenticationCookieName
{
get
{
return CustomAuthenticationProvider.formsAuthenticationCookieName;
}
set
{
CustomAuthenticationProvider.formsAuthenticationCookieName = value;
}
}
/// <summary>Enables forms authentication for the application</summary>
/// <param name="pipelines">Pipelines to add handlers to (usually "this")</param>
/// <param name="configuration">Forms authentication configuration</param>
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));
}
/// <summary>Enables forms authentication for a module</summary>
/// <param name="module">Module to add handlers to (usually "this")</param>
/// <param name="configuration">Forms authentication configuration</param>
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));
}
/// <summary>
/// Creates a response that sets the authentication cookie and redirects
/// the user back to where they came from.
/// </summary>
/// <param name="context">Current context</param>
/// <param name="userIdentifier">User identifier guid</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <param name="fallbackRedirectUrl">Url to redirect to if none in the querystring</param>
/// <returns>Nancy response with redirect.</returns>
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;
}
/// <summary>
/// Logs the user in.
/// </summary>
/// <param name="userIdentifier">User identifier guid</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <returns>Nancy response with status <see cref="HttpStatusCode.OK"/></returns>
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;
}
/// <summary>
/// Logs the user out and redirects them to a URL
/// </summary>
/// <param name="context">Current context</param>
/// <param name="redirectUrl">URL to redirect to</param>
/// <returns>Nancy response</returns>
public static Response LogOutAndRedirectResponse(NancyContext context, string redirectUrl)
{
var response = context.GetRedirect(redirectUrl);
var authenticationCookie = BuildLogoutCookie(currentConfiguration);
response.WithCookie(authenticationCookie);
return response;
}
/// <summary>
/// Logs the user out.
/// </summary>
/// <returns>Nancy response</returns>
public static Response LogOutResponse()
{
var response =
(Response)HttpStatusCode.OK;
var authenticationCookie =
BuildLogoutCookie(currentConfiguration);
response.WithCookie(authenticationCookie);
return response;
}
/// <summary>
/// Gets the pre request hook for loading the authenticated user's details
/// from the cookie.
/// </summary>
/// <param name="configuration">Forms authentication configuration to use</param>
/// <returns>Pre request hook delegate</returns>
private static Func<NancyContext, Response> 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;
};
}
/// <summary>
/// Gets the post request hook for redirecting to the login page
/// </summary>
/// <param name="configuration">Forms authentication configuration to use</param>
/// <returns>Post request hook delegate</returns>
private static Action<NancyContext> 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))));
}
};
}
/// <summary>
/// Gets the authenticated user GUID from the incoming request cookie if it exists
/// and is valid.
/// </summary>
/// <param name="context">Current context</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Returns user guid, or Guid.Empty if not present or invalid</returns>
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;
}
/// <summary>
/// Build the forms authentication cookie
/// </summary>
/// <param name="userIdentifier">Authenticated user identifier</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Nancy cookie instance</returns>
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;
}
/// <summary>
/// Builds a cookie for logging a user out
/// </summary>
/// <param name="configuration">Current configuration</param>
/// <returns>Nancy cookie instance</returns>
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;
}
/// <summary>
/// Encrypt and sign the cookie contents
/// </summary>
/// <param name="cookieValue">Plain text cookie value</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Encrypted and signed string</returns>
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);
}
/// <summary>
/// Generate a hmac for the encrypted cookie string
/// </summary>
/// <param name="encryptedCookie">Encrypted cookie string</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Hmac byte array</returns>
private static byte[] GenerateHmac(string encryptedCookie, CustomAuthenticationConfiguration configuration)
{
return configuration.CryptographyConfiguration.HmacProvider.GenerateHmac(encryptedCookie);
}
/// <summary>
/// Decrypt and validate an encrypted and signed cookie value
/// </summary>
/// <param name="cookieValue">Encrypted and signed cookie value</param>
/// <param name="configuration">Current configuration</param>
/// <returns>Decrypted value, or empty on error or if failed validation</returns>
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;
}
/// <summary>
/// Gets the redirect query string key from <see cref="FormsAuthenticationConfiguration"/>
/// </summary>
/// <param name="configuration">The forms authentication configuration.</param>
/// <returns>Redirect Querystring key</returns>
private static string GetRedirectQuerystringKey(CustomAuthenticationConfiguration configuration)
{
string redirectQuerystringKey = null;
if (configuration != null)
{
redirectQuerystringKey = configuration.RedirectQuerystringKey;
}
if (string.IsNullOrWhiteSpace(redirectQuerystringKey))
{
redirectQuerystringKey = CustomAuthenticationConfiguration.DefaultRedirectQuerystringKey;
}
return redirectQuerystringKey;
}
}
}

@ -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
{
/// <summary>
/// Logs the user in and returns either an empty 200 response for ajax requests, or a redirect response for non-ajax. <seealso cref="M:Nancy.Extensions.RequestExtensions.IsAjaxRequest(Nancy.Request)" />
/// </summary>
/// <param name="module">Nancy module</param>
/// <param name="userIdentifier">User identifier guid</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <param name="fallbackRedirectUrl">Url to redirect to if none in the querystring</param>
/// <returns>Nancy response with redirect if request was not ajax, otherwise with OK.</returns>
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);
}
/// <summary>
/// Logs the user in with the given user guid and redirects.
/// </summary>
/// <param name="module">Nancy module</param>
/// <param name="userIdentifier">User identifier guid</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <param name="fallbackRedirectUrl">Url to redirect to if none in the querystring</param>
/// <returns>Nancy response instance</returns>
public static Response LoginAndRedirect(this INancyModule module, Guid userIdentifier, DateTime? cookieExpiry = null, string fallbackRedirectUrl = "/")
{
return CustomAuthenticationProvider.UserLoggedInRedirectResponse(module.Context, userIdentifier, cookieExpiry, fallbackRedirectUrl);
}
/// <summary>
/// Logs the user in with the given user guid and returns ok response.
/// </summary>
/// <param name="module">Nancy module</param>
/// <param name="userIdentifier">User identifier guid</param>
/// <param name="cookieExpiry">Optional expiry date for the cookie (for 'Remember me')</param>
/// <returns>Nancy response instance</returns>
public static Response LoginWithoutRedirect(this INancyModule module, Guid userIdentifier, DateTime? cookieExpiry = null)
{
return CustomAuthenticationProvider.UserLoggedInResponse(userIdentifier, cookieExpiry);
}
/// <summary>
/// Logs the user out and returns either an empty 200 response for ajax requests, or a redirect response for non-ajax. <seealso cref="M:Nancy.Extensions.RequestExtensions.IsAjaxRequest(Nancy.Request)" />
/// </summary>
/// <param name="module">Nancy module</param>
/// <param name="redirectUrl">URL to redirect to</param>
/// <returns>Nancy response with redirect if request was not ajax, otherwise with OK.</returns>
public static Response Logout(this INancyModule module, string redirectUrl)
{
if (!module.Context.Request.IsAjaxRequest())
return CustomAuthenticationProvider.LogOutAndRedirectResponse(module.Context, redirectUrl);
return CustomAuthenticationProvider.LogOutResponse();
}
/// <summary>Logs the user out and redirects</summary>
/// <param name="module">Nancy module</param>
/// <param name="redirectUrl">URL to redirect to</param>
/// <returns>Nancy response instance</returns>
public static Response LogoutAndRedirect(this INancyModule module, string redirectUrl)
{
return CustomAuthenticationProvider.LogOutAndRedirectResponse(module.Context, redirectUrl);
}
/// <summary>Logs the user out without a redirect</summary>
/// <param name="module">Nancy module</param>
/// <returns>Nancy response instance</returns>
public static Response LogoutWithoutRedirect(this INancyModule module)
{
return CustomAuthenticationProvider.LogOutResponse();
}
}
}

@ -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<IUserMapper>()
PlexUserRepository = container.Get<IPlexUserRepository>(),
LocalUserRepository = container.Get<IUserRepository>()
};
FormsAuthentication.Enable(pipelines, formsAuthConfiguration);
CustomAuthenticationProvider.Enable(pipelines, config);
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;
ServicePointManager.ServerCertificateValidationCallback +=

@ -44,7 +44,8 @@ namespace PlexRequests.UI.Helpers
{
var userRepo = ServiceLocator.Instance.Resolve<IUserRepository>();
var linker = ServiceLocator.Instance.Resolve<IResourceLinker>();
return _security ?? (_security = new SecurityExtensions(userRepo, null, linker));
var plex = ServiceLocator.Instance.Resolve<IPlexUserRepository>();
return _security ?? (_security = new SecurityExtensions(userRepo, null, linker, plex));
}
}

@ -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<NancyContext, Response> ForbiddenIfNot(Func<NancyContext, bool> 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<NancyContext, Response> HttpStatusCodeIfNot(HttpStatusCode statusCode, Func<NancyContext, bool> test);
bool IsLoggedIn(NancyContext context);
bool IsNormalUser(NancyContext context);
bool IsPlexUser(NancyContext context);
}
}

@ -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;
}
}
}

@ -63,7 +63,8 @@ namespace PlexRequests.UI.Jobs
{
JobBuilder.Create<PlexAvailabilityChecker>().WithIdentity("PlexAvailabilityChecker", "Plex").Build(),
JobBuilder.Create<PlexContentCacher>().WithIdentity("PlexContentCacher", "Plex").Build(),
JobBuilder.Create<PlexEpisodeCacher>().WithIdentity("PlexEpisodeCacher", "Cache").Build(),
JobBuilder.Create<PlexEpisodeCacher>().WithIdentity("PlexEpisodeCacher", "Plex").Build(),
JobBuilder.Create<PlexUserChecker>().WithIdentity("PlexUserChecker", "Plex").Build(),
JobBuilder.Create<SickRageCacher>().WithIdentity("SickRageCacher", "Cache").Build(),
JobBuilder.Create<SonarrCacher>().WithIdentity("SonarrCacher", "Cache").Build(),
JobBuilder.Create<CouchPotatoCacher>().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<ITrigger>();
@ -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;
}

@ -124,7 +124,8 @@ namespace PlexRequests.UI.Modules
ICacheProvider cache, ISettingsService<SlackNotificationSettings> slackSettings,
ISlackApi slackApi, ISettingsService<LandingPageSettings> lp,
ISettingsService<ScheduledJobsSettings> scheduler, IJobRecord rec, IAnalytics analytics,
ISettingsService<NotificationSettingsV2> notifyService, IRecentlyAdded recentlyAdded) : base("admin", prService)
ISettingsService<NotificationSettingsV2> notifyService, IRecentlyAdded recentlyAdded
, ISecurityExtensions security) : base("admin", prService, security)
{
PrService = prService;
CpService = cpService;

@ -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<PlexRequestSettings> settingsService, ICacheProvider cache, IRepository<RequestQueue> requestQueue) : base("admin", settingsService)
public FaultQueueModule(ISettingsService<PlexRequestSettings> settingsService, ICacheProvider cache, IRepository<RequestQueue> requestQueue, ISecurityExtensions security) : base("admin", settingsService, security)
{
Cache = cache;
RequestQueue = requestQueue;

@ -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<PlexRequestSettings> settingsService, ICacheProvider cache, ISettingsService<SystemSettings> ss) : base("admin", settingsService)
public SystemStatusModule(ISettingsService<PlexRequestSettings> settingsService, ICacheProvider cache, ISettingsService<SystemSettings> ss, ISecurityExtensions security) : base("admin", settingsService, security)
{
Cache = cache;
SystemSettings = ss;

@ -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<PlexRequestSettings> pr) : base("apidocs", pr)
public ApiDocsModule(ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base("apidocs", pr, security)
{
Get["/"] = x => Documentation();
}

@ -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<PlexRequestSettings> pr) : base("api", pr)
public ApiRequestModule(IRequestService service, ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base("api", pr, security)
{
Get["GetRequests","/requests"] = x => GetRequests();
Get["GetRequest","/requests/{id}"] = x => GetSingleRequests(x);

@ -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<PlexRequestSettings> pr, ISettingsService<AuthenticationSettings> auth,
ISettingsService<PlexSettings> plexSettings, ISettingsService<CouchPotatoSettings> cp,
ISettingsService<SonarrSettings> sonarr, ISettingsService<SickRageSettings> sr, ISettingsService<HeadphonesSettings> hp) : base("api", pr)
ISettingsService<SonarrSettings> sonarr, ISettingsService<SickRageSettings> sr, ISettingsService<HeadphonesSettings> hp, ISecurityExtensions security) : base("api", pr, security)
{
Get["GetVersion", "/version"] = x => GetVersion();

@ -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<PlexRequestSettings> pr, ICustomUserMapper m) : base("api", pr)
public ApiUserModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, ISecurityExtensions security) : base("api", pr, security)
{
Put["PutCredentials", "/credentials/{username}"] = x => ChangePassword(x);

@ -47,7 +47,7 @@ namespace PlexRequests.UI.Modules
{
public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPlexApi plexApi,
ISickRageApi srApi, IHeadphonesApi hpApi, ISettingsService<PlexRequestSettings> pr) : base("test", pr)
ISickRageApi srApi, IHeadphonesApi hpApi, ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base("test", pr, security)
{
this.RequiresAuthentication();

@ -52,7 +52,8 @@ namespace PlexRequests.UI.Modules
public ApprovalModule(IRequestService service, ISettingsService<CouchPotatoSettings> cpService, ICouchPotatoApi cpApi, ISonarrApi sonarrApi,
ISettingsService<SonarrSettings> sonarrSettings, ISickRageApi srApi, ISettingsService<SickRageSettings> srSettings,
ISettingsService<HeadphonesSettings> hpSettings, IHeadphonesApi hpApi, ISettingsService<PlexRequestSettings> pr, ITransientFaultQueue faultQueue) : base("approval", pr)
ISettingsService<HeadphonesSettings> hpSettings, IHeadphonesApi hpApi, ISettingsService<PlexRequestSettings> 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);

@ -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<PlexRequestSettings> s) : base(s)
protected BaseApiModule(ISettingsService<PlexRequestSettings> s, ISecurityExtensions security) : base(s,security)
{
Settings = s;
Before += (ctx) => CheckAuth();
}
protected BaseApiModule(string modulePath, ISettingsService<PlexRequestSettings> s) : base(modulePath, s)
protected BaseApiModule(string modulePath, ISettingsService<PlexRequestSettings> s, ISecurityExtensions security) : base(modulePath, s, security)
{
Settings = s;
Before += (ctx) => CheckAuth();

@ -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<PlexRequestSettings> pr) : base(pr)
protected BaseAuthModule(ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base(pr,security)
{
PlexRequestSettings = pr;
Before += (ctx) => CheckAuth();
}
protected BaseAuthModule(string modulePath, ISettingsService<PlexRequestSettings> pr) : base(modulePath, pr)
protected BaseAuthModule(string modulePath, ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base(modulePath, pr, security)
{
PlexRequestSettings = pr;
Before += (ctx) => CheckAuth();

@ -49,7 +49,7 @@ namespace PlexRequests.UI.Modules
protected string BaseUrl { get; set; }
protected BaseModule(ISettingsService<PlexRequestSettings> settingsService)
protected BaseModule(ISettingsService<PlexRequestSettings> 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<PlexRequestSettings> settingsService)
protected BaseModule(string modulePath, ISettingsService<PlexRequestSettings> 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<IUserRepository>();
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<IUserRepository>();
var linker = ServiceLocator.Instance.Resolve<IResourceLinker>();
return _security ?? (_security = new SecurityExtensions(userRepo, this, linker));
}
}
private SecurityExtensions _security;
protected ISecurityExtensions Security { get; set; }
protected bool LoggedIn => Context?.CurrentUser != null;

@ -41,7 +41,7 @@ namespace PlexRequests.UI.Modules
{
public class CultureModule : BaseModule
{
public CultureModule(ISettingsService<PlexRequestSettings> pr, IAnalytics a) : base("culture",pr)
public CultureModule(ISettingsService<PlexRequestSettings> pr, IAnalytics a, ISecurityExtensions security) : base("culture",pr, security)
{
Analytics = a;

@ -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<PlexRequestSettings> pr) : base("customDonation", pr)
public DonationLinkModule(ICacheProvider provider, ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base("customDonation", pr, security)
{
Cache = provider;

@ -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<PlexRequestSettings> pr, ISettingsService<LandingPageSettings> l, IResourceLinker rl) : base(pr)
public IndexModule(ISettingsService<PlexRequestSettings> pr, ISettingsService<LandingPageSettings> l, IResourceLinker rl, ISecurityExtensions security) : base(pr, security)
{
LandingPage = l;
Linker = rl;

@ -26,7 +26,7 @@ namespace PlexRequests.UI.Modules
{
public class IssuesModule : BaseAuthModule
{
public IssuesModule(ISettingsService<PlexRequestSettings> pr, IIssueService issueService, IRequestService request, INotificationService n) : base("issues", pr)
public IssuesModule(ISettingsService<PlexRequestSettings> pr, IIssueService issueService, IRequestService request, INotificationService n, ISecurityExtensions security) : base("issues", pr, security)
{
IssuesService = issueService;
RequestService = request;

@ -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<PlexRequestSettings> settingsService, ISettingsService<LandingPageSettings> landing,
ISettingsService<PlexSettings> ps, IPlexApi pApi, IResourceLinker linker) : base("landing", settingsService)
ISettingsService<PlexSettings> ps, IPlexApi pApi, IResourceLinker linker, ISecurityExtensions security) : base("landing", settingsService, security)
{
LandingSettings = landing;
PlexSettings = ps;

@ -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<PlexRequestSettings> pr, ISettingsService<SystemSettings> settings, IJobRecord rec) : base("layout", pr)
public LayoutModule(ICacheProvider provider, ISettingsService<PlexRequestSettings> pr, ISettingsService<SystemSettings> settings, IJobRecord rec, ISecurityExtensions security) : base("layout", pr, security)
{
Cache = provider;
SystemSettings = settings;

@ -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<PlexRequestSettings> pr, ICustomUserMapper m, IResourceLinker linker, IRepository<UserLogins> userLoginRepo)
: base(pr)
public LoginModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IResourceLinker linker, IRepository<UserLogins> 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<Permissions>.All(), 0);
Session[SessionKeys.UsernameKey] = username;
return this.LoginAndRedirect((Guid)userId);
return CustomModuleExtensions.LoginAndRedirect(this, (Guid)userId);
};
Get["/changepassword"] = _ => ChangePassword();

@ -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<PlexRequestSettings> prSettings,
ISettingsService<RequestSettings> requestSettings,
ISettingsService<PlexSettings> plex,
INotificationService notify,
ISettingsService<SonarrSettings> sonarrSettings,
ISettingsService<SickRageSettings> sickRageSettings,
ISettingsService<CouchPotatoSettings> 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<PlexRequestSettings> PrSettings { get; }
private ISettingsService<PlexSettings> PlexSettings { get; }
private ISettingsService<RequestSettings> RequestSettings { get; }
private ISettingsService<SonarrSettings> SonarrSettings { get; }
private ISettingsService<SickRageSettings> SickRageSettings { get; }
private ISettingsService<CouchPotatoSettings> 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<Response> GetPlexRequestSettings()
{
return Response.AsJson(await PrSettings.GetSettingsAsync());
}
private async Task<Response> GetRequestSettings()
{
return Response.AsJson(await RequestSettings.GetSettingsAsync());
}
private async Task<Response> 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<Response> 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<QualityModel> qualities = new List<QualityModel>();
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<Response> 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<Response> 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 });
}
/// <summary>
/// Reports the issue.
/// Comment can be null if the <c>IssueState != Other</c>
/// </summary>
/// <param name="requestId">The request identifier.</param>
/// <param name="issue">The issue.</param>
/// <param name="comment">The comment.</param>
/// <returns></returns>
private async Task<Response> 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<Response> 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<Response> 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<RequestViewModel> MapMoviesToView(List<RequestedModel> dbMovies, List<QualityModel> 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<List<QualityModel>> GetQualityProfiles()
{
var qualities = new List<QualityModel>();
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<IEnumerable<RequestedModel>> 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;
}
}
}

@ -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;

@ -82,8 +82,8 @@ namespace PlexRequests.UI.Modules
ICouchPotatoCacher cpCacher, ISonarrCacher sonarrCacher, ISickRageCacher sickRageCacher, IPlexApi plexApi,
ISettingsService<PlexSettings> plexService, ISettingsService<AuthenticationSettings> auth,
IRepository<UsersToNotify> u, ISettingsService<EmailNotificationSettings> email,
IIssueService issue, IAnalytics a, IRepository<RequestLimit> rl, ITransientFaultQueue tfQueue, IRepository<PlexContent> content)
: base("search", prSettings)
IIssueService issue, IAnalytics a, IRepository<RequestLimit> rl, ITransientFaultQueue tfQueue, IRepository<PlexContent> content, ISecurityExtensions security)
: base("search", prSettings, security)
{
Auth = auth;
PlexService = plexService;

@ -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<AuthenticationSettings> auth, IPlexApi api, ISettingsService<PlexSettings> plexSettings, ISettingsService<PlexRequestSettings> pr,
ISettingsService<LandingPageSettings> lp, IAnalytics a, IResourceLinker linker, IRepository<UserLogins> userLogins) : base("userlogin", pr)
ISettingsService<LandingPageSettings> lp, IAnalytics a, IResourceLinker linker, IRepository<UserLogins> 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> UserLogins { get; }
private IPlexUserRepository PlexUserRepository { get; }
private ICustomUserMapper CustomUserMapper { get; }
private static Logger Log = LogManager.GetCurrentClassLogger();
private async Task<Response> 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());
}

@ -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<PlexRequestSettings> pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService<PlexSettings> plex, IRepository<UserLogins> userLogins, IRepository<PlexUsers> plexRepo) : base("usermanagement", pr)
public UserManagementModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService<PlexSettings> plex, IRepository<UserLogins> 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> PlexSettings { get; }
private IRepository<UserLogins> UserLoginsRepo { get; }
private IRepository<PlexUsers> PlexUsersRepository { get; }
private IPlexUserRepository PlexUsersRepository { get; }
private ISettingsService<PlexRequestSettings> 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);

@ -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<PlexRequestSettings> pr, ISettingsService<PlexSettings> plex, IPlexApi plexApi,
ISettingsService<AuthenticationSettings> auth, ICustomUserMapper m, IAnalytics a) : base("wizard", pr)
ISettingsService<AuthenticationSettings> 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");
}
}
}

@ -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<INotificationEngine>().To<NotificationEngine>();
Bind<IStatusChecker>().To<StatusChecker>();
Bind<ISecurityExtensions>().To<SecurityExtensions>();
}
}
}

@ -48,6 +48,7 @@ namespace PlexRequests.UI.NinjectModules
Bind<IJobRecord>().To<JobRecord>();
Bind<IUserRepository>().To<UserRepository>();
Bind<IPlexUserRepository>().To<PlexUserRepository>();
}
}

@ -203,6 +203,9 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Authentication\CustomAuthenticationConfiguration.cs" />
<Compile Include="Authentication\CustomAuthenticationProvider.cs" />
<Compile Include="Authentication\CustomModuleExtensions.cs" />
<Compile Include="Bootstrapper.cs" />
<Compile Include="Helpers\BaseUrlHelper.cs" />
<Compile Include="Helpers\ContravariantBindingResolver.cs" />
@ -212,6 +215,7 @@
<Compile Include="Helpers\EmptyViewBase.cs" />
<Compile Include="Helpers\AngularViewBase.cs" />
<Compile Include="Helpers\HtmlSecurityHelper.cs" />
<Compile Include="Helpers\ISecurityExtensions.cs" />
<Compile Include="Helpers\SecurityExtensions.cs" />
<Compile Include="Helpers\ServiceLocator.cs" />
<Compile Include="Helpers\Themes.cs" />
@ -257,7 +261,6 @@
<Compile Include="Modules\DonationLinkModule.cs" />
<Compile Include="Modules\IssuesModule.cs" />
<Compile Include="Modules\LandingPageModule.cs" />
<Compile Include="Modules\RequestsBetaModule.cs" />
<Compile Include="Modules\LayoutModule.cs" />
<Compile Include="Modules\UserWizardModule.cs" />
<Compile Include="NinjectModules\ApiModule.cs" />

@ -32,6 +32,16 @@
<input type="text" class="form-control form-control-custom " id="PlexAvailabilityChecker" name="PlexAvailabilityChecker" value="@Model.PlexAvailabilityChecker">
</div>
<div class="form-group">
<label for="PlexContentCacher" class="control-label">Plex Content Cacher (min)</label>
<input type="text" class="form-control form-control-custom " id="PlexContentCacher" name="PlexContentCacher" value="@Model.PlexContentCacher">
</div>
<div class="form-group">
<label for="PlexUserChecker" class="control-label">Plex User Checker (hours)</label>
<input type="text" class="form-control form-control-custom " id="PlexUserChecker" name="PlexContentCacher" value="@Model.PlexUserChecker">
</div>
<div class="form-group">
<label for="CouchPotatoCacher" class="control-label">Couch Potato Cacher (min)</label>
<input type="text" class="form-control form-control-custom " id="CouchPotatoCacher" name="CouchPotatoCacher" value="@Model.CouchPotatoCacher">

Loading…
Cancel
Save