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")] [Migration(11000, "v1.10.0.0")]
public class Version1100 : BaseMigration, IMigration public class Version1100 : BaseMigration, IMigration
{ {
public Version1100(IUserRepository userRepo, IRequestService requestService, ISettingsService<LogSettings> log, IPlexApi plexApi, ISettingsService<PlexSettings> plexService, IRepository<PlexUsers> plexusers, public Version1100(IUserRepository userRepo, IRequestService requestService, ISettingsService<LogSettings> log, IPlexApi plexApi, ISettingsService<PlexSettings> plexService, IPlexUserRepository plexusers,
ISettingsService<PlexRequestSettings> prSettings, ISettingsService<UserManagementSettings> umSettings) ISettingsService<PlexRequestSettings> prSettings, ISettingsService<UserManagementSettings> umSettings, ISettingsService<ScheduledJobsSettings> sjs)
{ {
UserRepo = userRepo; UserRepo = userRepo;
RequestService = requestService; RequestService = requestService;
@ -54,6 +54,7 @@ namespace PlexRequests.Core.Migration.Migrations
PlexUsers = plexusers; PlexUsers = plexusers;
PlexRequestSettings = prSettings; PlexRequestSettings = prSettings;
UserManagementSettings = umSettings; UserManagementSettings = umSettings;
ScheduledJobSettings = sjs;
} }
public int Version => 11000; public int Version => 11000;
private IUserRepository UserRepo { get; } private IUserRepository UserRepo { get; }
@ -61,9 +62,10 @@ namespace PlexRequests.Core.Migration.Migrations
private ISettingsService<LogSettings> Log { get; } private ISettingsService<LogSettings> Log { get; }
private IPlexApi PlexApi { get; } private IPlexApi PlexApi { get; }
private ISettingsService<PlexSettings> PlexSettings { get; } private ISettingsService<PlexSettings> PlexSettings { get; }
private IRepository<PlexUsers> PlexUsers { get; } private IPlexUserRepository PlexUsers { get; }
private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; } private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; }
private ISettingsService<UserManagementSettings> UserManagementSettings { get; } private ISettingsService<UserManagementSettings> UserManagementSettings { get; }
private ISettingsService<ScheduledJobsSettings> ScheduledJobSettings { get; }
public void Start(IDbConnection con) public void Start(IDbConnection con)
{ {
@ -74,10 +76,21 @@ namespace PlexRequests.Core.Migration.Migrations
ResetLogLevel(); ResetLogLevel();
UpdatePlexUsers(); UpdatePlexUsers();
PopulateDefaultUserManagementSettings(); PopulateDefaultUserManagementSettings();
UpdateScheduledJobs();
UpdateSchema(con, Version); UpdateSchema(con, Version);
} }
private void UpdateScheduledJobs()
{
var settings = ScheduledJobSettings.GetSettings();
settings.PlexUserChecker = 24;
settings.PlexContentCacher = 60;
ScheduledJobSettings.SaveSettings(settings);
}
private void PopulateDefaultUserManagementSettings() private void PopulateDefaultUserManagementSettings()
{ {
var plexRequestSettings = PlexRequestSettings.GetSettings(); var plexRequestSettings = PlexRequestSettings.GetSettings();
@ -147,6 +160,9 @@ namespace PlexRequests.Core.Migration.Migrations
Permissions = permissions, Permissions = permissions,
Features = 0, Features = 0,
UserAlias = string.Empty, UserAlias = string.Empty,
EmailAddress = user.Email,
Username = user.Username,
LoginId = Guid.NewGuid().ToString()
}; };
PlexUsers.Insert(m); PlexUsers.Insert(m);
@ -171,6 +187,8 @@ namespace PlexRequests.Core.Migration.Migrations
con.AlterTable("PlexUsers", "ADD", "Permissions", true, "INTEGER"); con.AlterTable("PlexUsers", "ADD", "Permissions", true, "INTEGER");
con.AlterTable("PlexUsers", "ADD", "Features", 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 //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 string RecentlyAddedCron { get; set; }
public int FaultQueueHandler { get; set; } public int FaultQueueHandler { get; set; }
public int PlexContentCacher { 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 EpisodeCacher = "Plex Episode Cacher";
public const string RecentlyAddedEmail = "Recently Added Email Notification"; public const string RecentlyAddedEmail = "Recently Added Email Notification";
public const string FaultQueueHandler = "Request Fault Queue Handler"; 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\PlexEpisodeCacher.cs" />
<Compile Include="Jobs\RecentlyAdded.cs" /> <Compile Include="Jobs\RecentlyAdded.cs" />
<Compile Include="Jobs\StoreBackup.cs" /> <Compile Include="Jobs\StoreBackup.cs" />
<Compile Include="Jobs\PlexUserChecker.cs" />
<Compile Include="Jobs\StoreCleanup.cs" /> <Compile Include="Jobs\StoreCleanup.cs" />
<Compile Include="Jobs\CouchPotatoCacher.cs" /> <Compile Include="Jobs\CouchPotatoCacher.cs" />
<Compile Include="Jobs\PlexAvailabilityChecker.cs" /> <Compile Include="Jobs\PlexAvailabilityChecker.cs" />

@ -25,6 +25,7 @@
// ************************************************************************/ // ************************************************************************/
#endregion #endregion
using System;
using Dapper.Contrib.Extensions; using Dapper.Contrib.Extensions;
namespace PlexRequests.Store.Models namespace PlexRequests.Store.Models
@ -36,5 +37,8 @@ namespace PlexRequests.Store.Models
public string UserAlias { get; set; } public string UserAlias { get; set; }
public int Permissions { get; set; } public int Permissions { get; set; }
public int Features { 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\SettingsJsonRepository.cs" />
<Compile Include="Repository\RequestJsonRepository.cs" /> <Compile Include="Repository\RequestJsonRepository.cs" />
<Compile Include="Repository\GenericRepository.cs" /> <Compile Include="Repository\GenericRepository.cs" />
<Compile Include="Repository\PlexUserRepository.cs" />
<Compile Include="Repository\UserRepository.cs" /> <Compile Include="Repository\UserRepository.cs" />
<Compile Include="RequestedModel.cs" /> <Compile Include="RequestedModel.cs" />
<Compile Include="UserEntity.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, Id INTEGER PRIMARY KEY AUTOINCREMENT,
PlexUserId varchar(100) NOT NULL, PlexUserId varchar(100) NOT NULL,
UserAlias varchar(100) NOT NULL, UserAlias varchar(100) NOT NULL,
Permissions INTEGER, Permissions INTEGER,
Features INTEGER Features INTEGER,
Username VARCHAR(100),
EmailAddress VARCHAR(100),
LoginId VARCHAR(100)
); );
CREATE UNIQUE INDEX IF NOT EXISTS PlexUsers_Id ON PlexUsers (Id); 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 Nancy.Json;
using Ninject; using Ninject;
using PlexRequests.UI.Authentication;
namespace PlexRequests.UI namespace PlexRequests.UI
{ {
@ -92,13 +93,14 @@ namespace PlexRequests.UI
var redirect = string.IsNullOrEmpty(baseUrl) ? "~/login" : $"~/{baseUrl}/login"; var redirect = string.IsNullOrEmpty(baseUrl) ? "~/login" : $"~/{baseUrl}/login";
// Enable forms auth // Enable forms auth
var formsAuthConfiguration = new FormsAuthenticationConfiguration var config = new CustomAuthenticationConfiguration
{ {
RedirectUrl = redirect, 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.SecurityProtocol = SecurityProtocolType.Tls;
ServicePointManager.ServerCertificateValidationCallback += ServicePointManager.ServerCertificateValidationCallback +=

@ -44,7 +44,8 @@ namespace PlexRequests.UI.Helpers
{ {
var userRepo = ServiceLocator.Instance.Resolve<IUserRepository>(); var userRepo = ServiceLocator.Instance.Resolve<IUserRepository>();
var linker = ServiceLocator.Instance.Resolve<IResourceLinker>(); 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 #endregion
using System; using System;
using System.Collections.Generic;
using System.Linq;
using Nancy; using Nancy;
using Nancy.Extensions;
using Nancy.Linker; using Nancy.Linker;
using Nancy.Responses; using Nancy.Responses;
using Nancy.Security; using Nancy.Security;
using Ninject;
using PlexRequests.Helpers.Permissions; using PlexRequests.Helpers.Permissions;
using PlexRequests.Store.Repository; using PlexRequests.Store.Repository;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
namespace PlexRequests.UI.Helpers 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; UserRepository = userRepository;
Module = context; Module = context;
Linker = linker; Linker = linker;
PlexUsers = plexUsers;
} }
private IUserRepository UserRepository { get; } private IUserRepository UserRepository { get; }
private NancyModule Module { get; } private NancyModule Module { get; }
private IResourceLinker Linker { get; } private IResourceLinker Linker { get; }
private IPlexUserRepository PlexUsers { get; }
public bool IsLoggedIn(NancyContext context) public bool IsLoggedIn(NancyContext context)
{ {
@ -99,11 +97,7 @@ namespace PlexRequests.UI.Helpers
{ {
return ForbiddenIfNot(ctx => return ForbiddenIfNot(ctx =>
{ {
var dbUser = UserRepository.GetUserByUsername(ctx.CurrentUser.UserName); var permissions = GetPermissions(ctx.CurrentUser);
if (dbUser == null) return false;
var permissions = (Permissions)dbUser.Permissions;
var result = permissions.HasFlag((Permissions)perm); var result = permissions.HasFlag((Permissions)perm);
return !result; return !result;
}); });
@ -116,37 +110,21 @@ namespace PlexRequests.UI.Helpers
public bool DoesNotHavePermissions(Permissions perm, IUserIdentity currentUser) public bool DoesNotHavePermissions(Permissions perm, IUserIdentity currentUser)
{ {
var dbUser = UserRepository.GetUserByUsername(currentUser.UserName); var permissions = GetPermissions(currentUser);
if (dbUser == null) return false;
var permissions = (Permissions)dbUser.Permissions;
var result = permissions.HasFlag(perm); var result = permissions.HasFlag(perm);
return !result; return !result;
} }
public bool HasPermissions(IUserIdentity user, Permissions perm) public bool HasPermissions(IUserIdentity user, Permissions perm)
{ {
if (user == null) return false; var permissions = GetPermissions(user);
return permissions.HasFlag(perm);
var dbUser = UserRepository.GetUserByUsername(user.UserName);
if (dbUser == null) return false;
var permissions = (Permissions)dbUser.Permissions;
var result = permissions.HasFlag(perm);
return result;
} }
public bool HasAnyPermissions(IUserIdentity user, params Permissions[] 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) foreach (var p in perm)
{ {
var result = permissions.HasFlag(p); var result = permissions.HasFlag(p);
@ -165,13 +143,7 @@ namespace PlexRequests.UI.Helpers
var response = ForbiddenIfNot(ctx => var response = ForbiddenIfNot(ctx =>
{ {
if (ctx.CurrentUser == null) return false; var permissions = GetPermissions(ctx.CurrentUser);
var dbUser = UserRepository.GetUserByUsername(ctx.CurrentUser.UserName);
if (dbUser == null) return false;
var permissions = (Permissions) dbUser.Permissions;
var result = permissions.HasFlag(perm); var result = permissions.HasFlag(perm);
return result; 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<PlexAvailabilityChecker>().WithIdentity("PlexAvailabilityChecker", "Plex").Build(),
JobBuilder.Create<PlexContentCacher>().WithIdentity("PlexContentCacher", "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<SickRageCacher>().WithIdentity("SickRageCacher", "Cache").Build(),
JobBuilder.Create<SonarrCacher>().WithIdentity("SonarrCacher", "Cache").Build(), JobBuilder.Create<SonarrCacher>().WithIdentity("SonarrCacher", "Cache").Build(),
JobBuilder.Create<CouchPotatoCacher>().WithIdentity("CouchPotatoCacher", "Cache").Build(), JobBuilder.Create<CouchPotatoCacher>().WithIdentity("CouchPotatoCacher", "Cache").Build(),
@ -159,6 +160,10 @@ namespace PlexRequests.UI.Jobs
{ {
s.PlexContentCacher = 60; s.PlexContentCacher = 60;
} }
if (s.PlexUserChecker == 0)
{
s.PlexUserChecker = 24;
}
var triggers = new List<ITrigger>(); var triggers = new List<ITrigger>();
@ -175,6 +180,13 @@ namespace PlexRequests.UI.Jobs
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexContentCacher).RepeatForever()) .WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexContentCacher).RepeatForever())
.Build(); .Build();
var plexUserChecker =
TriggerBuilder.Create()
.WithIdentity("PlexUserChecker", "Plex")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexUserChecker).RepeatForever())
.Build();
var srCacher = var srCacher =
TriggerBuilder.Create() TriggerBuilder.Create()
.WithIdentity("SickRageCacher", "Cache") .WithIdentity("SickRageCacher", "Cache")
@ -253,6 +265,7 @@ namespace PlexRequests.UI.Jobs
triggers.Add(plexEpCacher); triggers.Add(plexEpCacher);
triggers.Add(fault); triggers.Add(fault);
triggers.Add(plexCacher); triggers.Add(plexCacher);
triggers.Add(plexUserChecker);
return triggers; return triggers;
} }

@ -124,7 +124,8 @@ namespace PlexRequests.UI.Modules
ICacheProvider cache, ISettingsService<SlackNotificationSettings> slackSettings, ICacheProvider cache, ISettingsService<SlackNotificationSettings> slackSettings,
ISlackApi slackApi, ISettingsService<LandingPageSettings> lp, ISlackApi slackApi, ISettingsService<LandingPageSettings> lp,
ISettingsService<ScheduledJobsSettings> scheduler, IJobRecord rec, IAnalytics analytics, 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; PrService = prService;
CpService = cpService; CpService = cpService;

@ -35,13 +35,14 @@ using PlexRequests.Helpers.Permissions;
using PlexRequests.Store; using PlexRequests.Store;
using PlexRequests.Store.Models; using PlexRequests.Store.Models;
using PlexRequests.Store.Repository; using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules.Admin namespace PlexRequests.UI.Modules.Admin
{ {
public class FaultQueueModule : BaseModule 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; Cache = cache;
RequestQueue = requestQueue; RequestQueue = requestQueue;

@ -38,13 +38,14 @@ using PlexRequests.Core.SettingModels;
using PlexRequests.Core.StatusChecker; using PlexRequests.Core.StatusChecker;
using PlexRequests.Helpers; using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions; using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules.Admin namespace PlexRequests.UI.Modules.Admin
{ {
public class SystemStatusModule : BaseModule 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; Cache = cache;
SystemSettings = ss; SystemSettings = ss;

@ -29,12 +29,13 @@ using Nancy.Responses.Negotiation;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class ApiDocsModule : BaseModule 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(); Get["/"] = x => Documentation();
} }

@ -37,13 +37,14 @@ using Newtonsoft.Json;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Store; using PlexRequests.Store;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class ApiRequestModule : BaseApiModule 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["GetRequests","/requests"] = x => GetRequests();
Get["GetRequest","/requests/{id}"] = x => GetSingleRequests(x); Get["GetRequest","/requests/{id}"] = x => GetSingleRequests(x);

@ -37,6 +37,7 @@ using Newtonsoft.Json;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers; using PlexRequests.Helpers;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
@ -44,7 +45,7 @@ namespace PlexRequests.UI.Modules
{ {
public ApiSettingsModule(ISettingsService<PlexRequestSettings> pr, ISettingsService<AuthenticationSettings> auth, public ApiSettingsModule(ISettingsService<PlexRequestSettings> pr, ISettingsService<AuthenticationSettings> auth,
ISettingsService<PlexSettings> plexSettings, ISettingsService<CouchPotatoSettings> cp, 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(); Get["GetVersion", "/version"] = x => GetVersion();

@ -33,13 +33,14 @@ using Nancy.ModelBinding;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Store; using PlexRequests.Store;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class ApiUserModule : BaseApiModule 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); Put["PutCredentials", "/credentials/{username}"] = x => ChangePassword(x);

@ -47,7 +47,7 @@ namespace PlexRequests.UI.Modules
{ {
public ApplicationTesterModule(ICouchPotatoApi cpApi, ISonarrApi sonarrApi, IPlexApi plexApi, 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(); this.RequiresAuthentication();

@ -52,7 +52,8 @@ namespace PlexRequests.UI.Modules
public ApprovalModule(IRequestService service, ISettingsService<CouchPotatoSettings> cpService, ICouchPotatoApi cpApi, ISonarrApi sonarrApi, public ApprovalModule(IRequestService service, ISettingsService<CouchPotatoSettings> cpService, ICouchPotatoApi cpApi, ISonarrApi sonarrApi,
ISettingsService<SonarrSettings> sonarrSettings, ISickRageApi srApi, ISettingsService<SickRageSettings> srSettings, 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); this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
@ -68,6 +69,7 @@ namespace PlexRequests.UI.Modules
SickRageSettings = srSettings; SickRageSettings = srSettings;
HeadphonesSettings = hpSettings; HeadphonesSettings = hpSettings;
HeadphoneApi = hpApi; HeadphoneApi = hpApi;
FaultQueue = faultQueue;
Post["/approve", true] = async (x, ct) => await Approve((int)Request.Form.requestid, (string)Request.Form.qualityId); 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); 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;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Store; using PlexRequests.Store;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public abstract class BaseApiModule : BaseModule public abstract class BaseApiModule : BaseModule
{ {
protected BaseApiModule(ISettingsService<PlexRequestSettings> s) : base(s) protected BaseApiModule(ISettingsService<PlexRequestSettings> s, ISecurityExtensions security) : base(s,security)
{ {
Settings = s; Settings = s;
Before += (ctx) => CheckAuth(); 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; Settings = s;
Before += (ctx) => CheckAuth(); Before += (ctx) => CheckAuth();

@ -31,18 +31,19 @@ using Nancy.Extensions;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public abstract class BaseAuthModule : BaseModule public abstract class BaseAuthModule : BaseModule
{ {
protected BaseAuthModule(ISettingsService<PlexRequestSettings> pr) : base(pr) protected BaseAuthModule(ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base(pr,security)
{ {
PlexRequestSettings = pr; PlexRequestSettings = pr;
Before += (ctx) => CheckAuth(); 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; PlexRequestSettings = pr;
Before += (ctx) => CheckAuth(); Before += (ctx) => CheckAuth();

@ -49,7 +49,7 @@ namespace PlexRequests.UI.Modules
protected string BaseUrl { get; set; } protected string BaseUrl { get; set; }
protected BaseModule(ISettingsService<PlexRequestSettings> settingsService) protected BaseModule(ISettingsService<PlexRequestSettings> settingsService, ISecurityExtensions security)
{ {
var settings = settingsService.GetSettings(); var settings = settingsService.GetSettings();
@ -59,11 +59,12 @@ namespace PlexRequests.UI.Modules
var modulePath = string.IsNullOrEmpty(baseUrl) ? string.Empty : baseUrl; var modulePath = string.IsNullOrEmpty(baseUrl) ? string.Empty : baseUrl;
ModulePath = modulePath; ModulePath = modulePath;
Security = security;
Before += (ctx) => SetCookie(); Before += (ctx) => SetCookie();
} }
protected BaseModule(string modulePath, ISettingsService<PlexRequestSettings> settingsService) protected BaseModule(string modulePath, ISettingsService<PlexRequestSettings> settingsService, ISecurityExtensions security)
{ {
var settings = settingsService.GetSettings(); var settings = settingsService.GetSettings();
@ -73,6 +74,7 @@ namespace PlexRequests.UI.Modules
var settingModulePath = string.IsNullOrEmpty(baseUrl) ? modulePath : $"{baseUrl}/{modulePath}"; var settingModulePath = string.IsNullOrEmpty(baseUrl) ? modulePath : $"{baseUrl}/{modulePath}";
ModulePath = settingModulePath; ModulePath = settingModulePath;
Security = security;
Before += (ctx) => Before += (ctx) =>
{ {
@ -100,8 +102,9 @@ namespace PlexRequests.UI.Modules
return _dateTimeOffset; return _dateTimeOffset;
} }
} }
private string _username;
private string _username;
protected string Username protected string Username
{ {
get get
@ -110,7 +113,7 @@ namespace PlexRequests.UI.Modules
{ {
try try
{ {
_username = Session[SessionKeys.UsernameKey].ToString(); _username = User == null ? Session[SessionKeys.UsernameKey].ToString() : User.UserName;
} }
catch (Exception) catch (Exception)
{ {
@ -131,33 +134,14 @@ namespace PlexRequests.UI.Modules
{ {
return false; return false;
} }
var userRepo = ServiceLocator.Instance.Resolve<IUserRepository>(); return Security.HasPermissions(Context?.CurrentUser, Permissions.Administrator);
var user = userRepo.GetUserByUsername(Context?.CurrentUser?.UserName);
if (user == null) return false;
var permissions = (Permissions) user.Permissions;
return permissions.HasFlag(Permissions.Administrator);
} }
} }
protected IUserIdentity User => Context?.CurrentUser; protected IUserIdentity User => Context?.CurrentUser;
protected SecurityExtensions Security protected ISecurityExtensions Security { get; set; }
{
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 bool LoggedIn => Context?.CurrentUser != null; protected bool LoggedIn => Context?.CurrentUser != null;

@ -41,7 +41,7 @@ namespace PlexRequests.UI.Modules
{ {
public class CultureModule : BaseModule 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; Analytics = a;

@ -7,12 +7,13 @@ using NLog;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers; using PlexRequests.Helpers;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class DonationLinkModule : BaseAuthModule 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; Cache = provider;

@ -33,12 +33,13 @@ using Nancy.Responses;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class IndexModule : BaseAuthModule 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; LandingPage = l;
Linker = rl; Linker = rl;

@ -26,7 +26,7 @@ namespace PlexRequests.UI.Modules
{ {
public class IssuesModule : BaseAuthModule 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; IssuesService = issueService;
RequestService = request; RequestService = request;

@ -33,6 +33,7 @@ using Nancy.Linker;
using PlexRequests.Api.Interfaces; using PlexRequests.Api.Interfaces;
using PlexRequests.Core; using PlexRequests.Core;
using PlexRequests.Core.SettingModels; using PlexRequests.Core.SettingModels;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
@ -40,7 +41,7 @@ namespace PlexRequests.UI.Modules
public class LandingPageModule : BaseModule public class LandingPageModule : BaseModule
{ {
public LandingPageModule(ISettingsService<PlexRequestSettings> settingsService, ISettingsService<LandingPageSettings> landing, 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; LandingSettings = landing;
PlexSettings = ps; PlexSettings = ps;

@ -38,13 +38,14 @@ using PlexRequests.Core.StatusChecker;
using PlexRequests.Helpers; using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces; using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Jobs; using PlexRequests.Services.Jobs;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class LayoutModule : BaseAuthModule 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; Cache = provider;
SystemSettings = settings; SystemSettings = settings;

@ -31,7 +31,6 @@ using System;
using System.Dynamic; using System.Dynamic;
using System.Security; using System.Security;
using Nancy; using Nancy;
using Nancy.Authentication.Forms;
using Nancy.Extensions; using Nancy.Extensions;
using Nancy.Linker; using Nancy.Linker;
using Nancy.Responses.Negotiation; using Nancy.Responses.Negotiation;
@ -43,14 +42,17 @@ using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions; using PlexRequests.Helpers.Permissions;
using PlexRequests.Store; using PlexRequests.Store;
using PlexRequests.Store.Repository; using PlexRequests.Store.Repository;
using PlexRequests.UI.Authentication;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
using ModuleExtensions = Nancy.Authentication.Forms.ModuleExtensions;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class LoginModule : BaseModule public class LoginModule : BaseModule
{ {
public LoginModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IResourceLinker linker, IRepository<UserLogins> userLoginRepo) public LoginModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IResourceLinker linker, IRepository<UserLogins> userLoginRepo, ISecurityExtensions security)
: base(pr) : base(pr, security)
{ {
UserMapper = m; UserMapper = m;
Get["LocalLogin","/login"] = _ => Get["LocalLogin","/login"] = _ =>
@ -74,7 +76,7 @@ namespace PlexRequests.UI.Modules
{ {
Session.Delete(SessionKeys.UsernameKey); Session.Delete(SessionKeys.UsernameKey);
} }
return this.LogoutAndRedirect(!string.IsNullOrEmpty(BaseUrl) ? $"~/{BaseUrl}/" : "~/"); return CustomModuleExtensions.LogoutAndRedirect(this, !string.IsNullOrEmpty(BaseUrl) ? $"~/{BaseUrl}/" : "~/");
}; };
Post["/login"] = x => Post["/login"] = x =>
@ -112,7 +114,7 @@ namespace PlexRequests.UI.Modules
UserId = userId.ToString() UserId = userId.ToString()
}); });
return this.LoginAndRedirect(userId.Value, expiry, redirect); return CustomModuleExtensions.LoginAndRedirect(this,userId.Value, expiry, redirect);
}; };
Get["/register"] = x => Get["/register"] = x =>
@ -138,7 +140,7 @@ namespace PlexRequests.UI.Modules
} }
var userId = UserMapper.CreateUser(username, Request.Form.Password, EnumHelper<Permissions>.All(), 0); var userId = UserMapper.CreateUser(username, Request.Form.Password, EnumHelper<Permissions>.All(), 0);
Session[SessionKeys.UsernameKey] = username; Session[SessionKeys.UsernameKey] = username;
return this.LoginAndRedirect((Guid)userId); return CustomModuleExtensions.LoginAndRedirect(this, (Guid)userId);
}; };
Get["/changepassword"] = _ => ChangePassword(); 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.Core.Models;
using PlexRequests.Helpers.Analytics; using PlexRequests.Helpers.Analytics;
using PlexRequests.Helpers.Permissions; using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Helpers;
using Action = PlexRequests.Helpers.Analytics.Action; using Action = PlexRequests.Helpers.Analytics.Action;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
@ -67,7 +68,8 @@ namespace PlexRequests.UI.Modules
ISickRageApi sickRageApi, ISickRageApi sickRageApi,
ICacheProvider cache, ICacheProvider cache,
IAnalytics an, IAnalytics an,
INotificationEngine engine) : base("requests", prSettings) INotificationEngine engine,
ISecurityExtensions security) : base("requests", prSettings, security)
{ {
Service = service; Service = service;
PrSettings = prSettings; PrSettings = prSettings;

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

@ -44,17 +44,19 @@ using PlexRequests.Helpers;
using PlexRequests.Helpers.Analytics; using PlexRequests.Helpers.Analytics;
using PlexRequests.Store; using PlexRequests.Store;
using PlexRequests.Store.Repository; using PlexRequests.Store.Repository;
using PlexRequests.UI.Authentication;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
using ModuleExtensions = Nancy.Authentication.Forms.ModuleExtensions;
using Action = PlexRequests.Helpers.Analytics.Action;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class UserLoginModule : BaseModule public class UserLoginModule : BaseModule
{ {
public UserLoginModule(ISettingsService<AuthenticationSettings> auth, IPlexApi api, ISettingsService<PlexSettings> plexSettings, ISettingsService<PlexRequestSettings> pr, 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; AuthService = auth;
LandingPageSettings = lp; LandingPageSettings = lp;
@ -63,6 +65,8 @@ namespace PlexRequests.UI.Modules
PlexSettings = plexSettings; PlexSettings = plexSettings;
Linker = linker; Linker = linker;
UserLogins = userLogins; UserLogins = userLogins;
PlexUserRepository = plexUsers;
CustomUserMapper = custom;
Get["UserLoginIndex", "/", true] = async (x, ct) => Get["UserLoginIndex", "/", true] = async (x, ct) =>
{ {
@ -86,12 +90,15 @@ namespace PlexRequests.UI.Modules
private IResourceLinker Linker { get; } private IResourceLinker Linker { get; }
private IAnalytics Analytics { get; } private IAnalytics Analytics { get; }
private IRepository<UserLogins> UserLogins { get; } private IRepository<UserLogins> UserLogins { get; }
private IPlexUserRepository PlexUserRepository { get; }
private ICustomUserMapper CustomUserMapper { get; }
private static Logger Log = LogManager.GetCurrentClassLogger(); private static Logger Log = LogManager.GetCurrentClassLogger();
private async Task<Response> LoginUser() private async Task<Response> LoginUser()
{ {
var userId = string.Empty; var userId = string.Empty;
var loginGuid = Guid.Empty;
var dateTimeOffset = Request.Form.DateTimeOffset; var dateTimeOffset = Request.Form.DateTimeOffset;
var username = Request.Form.username.Value; var username = Request.Form.username.Value;
Log.Debug("Username \"{0}\" attempting to login", username); Log.Debug("Username \"{0}\" attempting to login", username);
@ -122,6 +129,9 @@ namespace PlexRequests.UI.Modules
password = Request.Form.password.Value; password = Request.Form.password.Value;
} }
var localUsers = await CustomUserMapper.GetUsersAsync();
var plexLocalUsers = await PlexUserRepository.GetAllAsync();
if (settings.UserAuthentication && settings.UsePassword) // Authenticate with Plex if (settings.UserAuthentication && settings.UsePassword) // Authenticate with Plex
{ {
@ -172,6 +182,18 @@ namespace PlexRequests.UI.Modules
// Add to the session (Used in the BaseModules) // Add to the session (Used in the BaseModules)
Session[SessionKeys.UsernameKey] = (string)username; Session[SessionKeys.UsernameKey] = (string)username;
Session[SessionKeys.ClientDateTimeOffsetKey] = (int)dateTimeOffset; 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) if (!authenticated)
@ -188,10 +210,20 @@ namespace PlexRequests.UI.Modules
if (!landingSettings.BeforeLogin) if (!landingSettings.BeforeLogin)
{ {
var uri = Linker.BuildRelativeUri(Context, "LandingPageIndex"); var uri = Linker.BuildRelativeUri(Context, "LandingPageIndex");
if (loginGuid != Guid.Empty)
{
return CustomModuleExtensions.LoginAndRedirect(this, loginGuid, null, uri.ToString());
}
return Response.AsRedirect(uri.ToString()); return Response.AsRedirect(uri.ToString());
} }
} }
var retVal = Linker.BuildRelativeUri(Context, "SearchIndex"); var retVal = Linker.BuildRelativeUri(Context, "SearchIndex");
if (loginGuid != Guid.Empty)
{
return CustomModuleExtensions.LoginAndRedirect(this, loginGuid, null, retVal.ToString());
}
return Response.AsRedirect(retVal.ToString()); return Response.AsRedirect(retVal.ToString());
} }

@ -17,13 +17,15 @@ using PlexRequests.Helpers.Permissions;
using PlexRequests.Store; using PlexRequests.Store;
using PlexRequests.Store.Models; using PlexRequests.Store.Models;
using PlexRequests.Store.Repository; using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules namespace PlexRequests.UI.Modules
{ {
public class UserManagementModule : BaseModule 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 #if !DEBUG
Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx); Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx);
@ -51,7 +53,7 @@ namespace PlexRequests.UI.Modules
private IPlexApi PlexApi { get; } private IPlexApi PlexApi { get; }
private ISettingsService<PlexSettings> PlexSettings { get; } private ISettingsService<PlexSettings> PlexSettings { get; }
private IRepository<UserLogins> UserLoginsRepo { get; } private IRepository<UserLogins> UserLoginsRepo { get; }
private IRepository<PlexUsers> PlexUsersRepository { get; } private IPlexUserRepository PlexUsersRepository { get; }
private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; } private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; }
private Negotiator Load() private Negotiator Load()
@ -112,11 +114,21 @@ namespace PlexRequests.UI.Modules
{ {
return Response.AsJson(new JsonResponseModel return Response.AsJson(new JsonResponseModel
{ {
Result = true, Result = false,
Message = "Please enter in a valid Username and Password" 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 featuresVal = 0;
var permissionsVal = 0; var permissionsVal = 0;
@ -213,7 +225,10 @@ namespace PlexRequests.UI.Modules
Permissions = permissionsValue, Permissions = permissionsValue,
Features = featuresValue, Features = featuresValue,
UserAlias = model.Alias, UserAlias = model.Alias,
PlexUserId = plexUser.Id PlexUserId = plexUser.Id,
EmailAddress = plexUser.Email,
Username = plexUser.Username,
LoginId = Guid.NewGuid().ToString()
}; };
await PlexUsersRepository.InsertAsync(user); await PlexUsersRepository.InsertAsync(user);

@ -41,6 +41,7 @@ using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers; using PlexRequests.Helpers;
using PlexRequests.Helpers.Analytics; using PlexRequests.Helpers.Analytics;
using PlexRequests.Helpers.Permissions; using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Authentication;
using PlexRequests.UI.Helpers; using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models; using PlexRequests.UI.Models;
@ -51,7 +52,7 @@ namespace PlexRequests.UI.Modules
public class UserWizardModule : BaseModule public class UserWizardModule : BaseModule
{ {
public UserWizardModule(ISettingsService<PlexRequestSettings> pr, ISettingsService<PlexSettings> plex, IPlexApi plexApi, 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; PlexSettings = plex;
PlexApi = plexApi; PlexApi = plexApi;
@ -200,7 +201,7 @@ namespace PlexRequests.UI.Modules
var baseUrl = string.IsNullOrEmpty(settings.BaseUrl) ? string.Empty : $"/{settings.BaseUrl}"; 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.Interfaces;
using PlexRequests.Services.Notification; using PlexRequests.Services.Notification;
using PlexRequests.Store; using PlexRequests.Store;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.NinjectModules namespace PlexRequests.UI.NinjectModules
{ {
@ -59,6 +60,8 @@ namespace PlexRequests.UI.NinjectModules
Bind<INotificationEngine>().To<NotificationEngine>(); Bind<INotificationEngine>().To<NotificationEngine>();
Bind<IStatusChecker>().To<StatusChecker>(); Bind<IStatusChecker>().To<StatusChecker>();
Bind<ISecurityExtensions>().To<SecurityExtensions>();
} }
} }
} }

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

@ -203,6 +203,9 @@
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Authentication\CustomAuthenticationConfiguration.cs" />
<Compile Include="Authentication\CustomAuthenticationProvider.cs" />
<Compile Include="Authentication\CustomModuleExtensions.cs" />
<Compile Include="Bootstrapper.cs" /> <Compile Include="Bootstrapper.cs" />
<Compile Include="Helpers\BaseUrlHelper.cs" /> <Compile Include="Helpers\BaseUrlHelper.cs" />
<Compile Include="Helpers\ContravariantBindingResolver.cs" /> <Compile Include="Helpers\ContravariantBindingResolver.cs" />
@ -212,6 +215,7 @@
<Compile Include="Helpers\EmptyViewBase.cs" /> <Compile Include="Helpers\EmptyViewBase.cs" />
<Compile Include="Helpers\AngularViewBase.cs" /> <Compile Include="Helpers\AngularViewBase.cs" />
<Compile Include="Helpers\HtmlSecurityHelper.cs" /> <Compile Include="Helpers\HtmlSecurityHelper.cs" />
<Compile Include="Helpers\ISecurityExtensions.cs" />
<Compile Include="Helpers\SecurityExtensions.cs" /> <Compile Include="Helpers\SecurityExtensions.cs" />
<Compile Include="Helpers\ServiceLocator.cs" /> <Compile Include="Helpers\ServiceLocator.cs" />
<Compile Include="Helpers\Themes.cs" /> <Compile Include="Helpers\Themes.cs" />
@ -257,7 +261,6 @@
<Compile Include="Modules\DonationLinkModule.cs" /> <Compile Include="Modules\DonationLinkModule.cs" />
<Compile Include="Modules\IssuesModule.cs" /> <Compile Include="Modules\IssuesModule.cs" />
<Compile Include="Modules\LandingPageModule.cs" /> <Compile Include="Modules\LandingPageModule.cs" />
<Compile Include="Modules\RequestsBetaModule.cs" />
<Compile Include="Modules\LayoutModule.cs" /> <Compile Include="Modules\LayoutModule.cs" />
<Compile Include="Modules\UserWizardModule.cs" /> <Compile Include="Modules\UserWizardModule.cs" />
<Compile Include="NinjectModules\ApiModule.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"> <input type="text" class="form-control form-control-custom " id="PlexAvailabilityChecker" name="PlexAvailabilityChecker" value="@Model.PlexAvailabilityChecker">
</div> </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"> <div class="form-group">
<label for="CouchPotatoCacher" class="control-label">Couch Potato Cacher (min)</label> <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"> <input type="text" class="form-control form-control-custom " id="CouchPotatoCacher" name="CouchPotatoCacher" value="@Model.CouchPotatoCacher">

Loading…
Cancel
Save