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

This commit is contained in:
Jamie.Rees 2016-11-18 17:17:35 +00:00
parent 63c2744336
commit 2a8927eb6d
46 changed files with 1132 additions and 565 deletions

View file

@ -43,8 +43,8 @@ namespace PlexRequests.Core.Migration.Migrations
[Migration(11000, "v1.10.0.0")]
public class Version1100 : BaseMigration, IMigration
{
public Version1100(IUserRepository userRepo, IRequestService requestService, ISettingsService<LogSettings> log, IPlexApi plexApi, ISettingsService<PlexSettings> plexService, IRepository<PlexUsers> plexusers,
ISettingsService<PlexRequestSettings> prSettings, ISettingsService<UserManagementSettings> umSettings)
public Version1100(IUserRepository userRepo, IRequestService requestService, ISettingsService<LogSettings> log, IPlexApi plexApi, ISettingsService<PlexSettings> plexService, IPlexUserRepository plexusers,
ISettingsService<PlexRequestSettings> prSettings, ISettingsService<UserManagementSettings> umSettings, ISettingsService<ScheduledJobsSettings> sjs)
{
UserRepo = userRepo;
RequestService = requestService;
@ -54,6 +54,7 @@ namespace PlexRequests.Core.Migration.Migrations
PlexUsers = plexusers;
PlexRequestSettings = prSettings;
UserManagementSettings = umSettings;
ScheduledJobSettings = sjs;
}
public int Version => 11000;
private IUserRepository UserRepo { get; }
@ -61,9 +62,10 @@ namespace PlexRequests.Core.Migration.Migrations
private ISettingsService<LogSettings> Log { get; }
private IPlexApi PlexApi { get; }
private ISettingsService<PlexSettings> PlexSettings { get; }
private IRepository<PlexUsers> PlexUsers { get; }
private IPlexUserRepository PlexUsers { get; }
private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; }
private ISettingsService<UserManagementSettings> UserManagementSettings { get; }
private ISettingsService<ScheduledJobsSettings> ScheduledJobSettings { get; }
public void Start(IDbConnection con)
{
@ -74,10 +76,21 @@ namespace PlexRequests.Core.Migration.Migrations
ResetLogLevel();
UpdatePlexUsers();
PopulateDefaultUserManagementSettings();
UpdateScheduledJobs();
UpdateSchema(con, Version);
}
private void UpdateScheduledJobs()
{
var settings = ScheduledJobSettings.GetSettings();
settings.PlexUserChecker = 24;
settings.PlexContentCacher = 60;
ScheduledJobSettings.SaveSettings(settings);
}
private void PopulateDefaultUserManagementSettings()
{
var plexRequestSettings = PlexRequestSettings.GetSettings();
@ -147,6 +160,9 @@ namespace PlexRequests.Core.Migration.Migrations
Permissions = permissions,
Features = 0,
UserAlias = string.Empty,
EmailAddress = user.Email,
Username = user.Username,
LoginId = Guid.NewGuid().ToString()
};
PlexUsers.Insert(m);
@ -171,6 +187,8 @@ namespace PlexRequests.Core.Migration.Migrations
con.AlterTable("PlexUsers", "ADD", "Permissions", true, "INTEGER");
con.AlterTable("PlexUsers", "ADD", "Features", true, "INTEGER");
con.AlterTable("PlexUsers", "ADD", "Username", true, "VARCHAR(100)");
con.AlterTable("PlexUsers", "ADD", "EmailAddress", true, "VARCHAR(100)");
//https://image.tmdb.org/t/p/w150/https://image.tmdb.org/t/p/w150//aqhAqttDq7zgsTaBHtCD8wmTk6k.jpg

View file

@ -44,5 +44,6 @@ namespace PlexRequests.Core.SettingModels
public string RecentlyAddedCron { get; set; }
public int FaultQueueHandler { get; set; }
public int PlexContentCacher { get; set; }
public int PlexUserChecker { get; set; }
}
}

View file

@ -39,5 +39,7 @@ namespace PlexRequests.Services.Jobs
public const string EpisodeCacher = "Plex Episode Cacher";
public const string RecentlyAddedEmail = "Recently Added Email Notification";
public const string FaultQueueHandler = "Request Fault Queue Handler";
public const string PlexUserChecker = "Plex User Checker";
}
}

View file

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

View file

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

View file

@ -25,6 +25,7 @@
// ************************************************************************/
#endregion
using System;
using Dapper.Contrib.Extensions;
namespace PlexRequests.Store.Models
@ -36,5 +37,8 @@ namespace PlexRequests.Store.Models
public string UserAlias { get; set; }
public int Permissions { get; set; }
public int Features { get; set; }
public string Username { get; set; }
public string EmailAddress { get; set; }
public string LoginId { get; set; }
}
}

View file

@ -88,6 +88,7 @@
<Compile Include="Repository\SettingsJsonRepository.cs" />
<Compile Include="Repository\RequestJsonRepository.cs" />
<Compile Include="Repository\GenericRepository.cs" />
<Compile Include="Repository\PlexUserRepository.cs" />
<Compile Include="Repository\UserRepository.cs" />
<Compile Include="RequestedModel.cs" />
<Compile Include="UserEntity.cs" />

View file

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

View file

@ -117,7 +117,10 @@ CREATE TABLE IF NOT EXISTS PlexUsers
PlexUserId varchar(100) NOT NULL,
UserAlias varchar(100) NOT NULL,
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);

View file

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

View file

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

View file

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

View file

@ -52,6 +52,7 @@ using PlexRequests.UI.Helpers;
using Nancy.Json;
using Ninject;
using PlexRequests.UI.Authentication;
namespace PlexRequests.UI
{
@ -92,13 +93,14 @@ namespace PlexRequests.UI
var redirect = string.IsNullOrEmpty(baseUrl) ? "~/login" : $"~/{baseUrl}/login";
// Enable forms auth
var formsAuthConfiguration = new FormsAuthenticationConfiguration
var config = new CustomAuthenticationConfiguration
{
RedirectUrl = redirect,
UserMapper = container.Get<IUserMapper>()
PlexUserRepository = container.Get<IPlexUserRepository>(),
LocalUserRepository = container.Get<IUserRepository>()
};
FormsAuthentication.Enable(pipelines, formsAuthConfiguration);
CustomAuthenticationProvider.Enable(pipelines, config);
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;
ServicePointManager.ServerCertificateValidationCallback +=

View file

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

View file

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

View file

@ -26,32 +26,30 @@
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using Nancy;
using Nancy.Extensions;
using Nancy.Linker;
using Nancy.Responses;
using Nancy.Security;
using Ninject;
using PlexRequests.Helpers.Permissions;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Models;
namespace PlexRequests.UI.Helpers
{
public class SecurityExtensions
public class SecurityExtensions : ISecurityExtensions
{
public SecurityExtensions(IUserRepository userRepository, NancyModule context, IResourceLinker linker)
public SecurityExtensions(IUserRepository userRepository, NancyModule context, IResourceLinker linker, IPlexUserRepository plexUsers)
{
UserRepository = userRepository;
Module = context;
Linker = linker;
PlexUsers = plexUsers;
}
private IUserRepository UserRepository { get; }
private NancyModule Module { get; }
private IResourceLinker Linker { get; }
private IPlexUserRepository PlexUsers { get; }
public bool IsLoggedIn(NancyContext context)
{
@ -99,11 +97,7 @@ namespace PlexRequests.UI.Helpers
{
return ForbiddenIfNot(ctx =>
{
var dbUser = UserRepository.GetUserByUsername(ctx.CurrentUser.UserName);
if (dbUser == null) return false;
var permissions = (Permissions)dbUser.Permissions;
var permissions = GetPermissions(ctx.CurrentUser);
var result = permissions.HasFlag((Permissions)perm);
return !result;
});
@ -116,37 +110,21 @@ namespace PlexRequests.UI.Helpers
public bool DoesNotHavePermissions(Permissions perm, IUserIdentity currentUser)
{
var dbUser = UserRepository.GetUserByUsername(currentUser.UserName);
if (dbUser == null) return false;
var permissions = (Permissions)dbUser.Permissions;
var permissions = GetPermissions(currentUser);
var result = permissions.HasFlag(perm);
return !result;
}
public bool HasPermissions(IUserIdentity user, Permissions perm)
{
if (user == null) return false;
var dbUser = UserRepository.GetUserByUsername(user.UserName);
if (dbUser == null) return false;
var permissions = (Permissions)dbUser.Permissions;
var result = permissions.HasFlag(perm);
return result;
var permissions = GetPermissions(user);
return permissions.HasFlag(perm);
}
public bool HasAnyPermissions(IUserIdentity user, params Permissions[] perm)
{
if (user == null) return false;
var permissions = GetPermissions(user);
var dbUser = UserRepository.GetUserByUsername(user.UserName);
if (dbUser == null) return false;
var permissions = (Permissions)dbUser.Permissions;
foreach (var p in perm)
{
var result = permissions.HasFlag(p);
@ -165,13 +143,7 @@ namespace PlexRequests.UI.Helpers
var response = ForbiddenIfNot(ctx =>
{
if (ctx.CurrentUser == null) return false;
var dbUser = UserRepository.GetUserByUsername(ctx.CurrentUser.UserName);
if (dbUser == null) return false;
var permissions = (Permissions) dbUser.Permissions;
var permissions = GetPermissions(ctx.CurrentUser);
var result = permissions.HasFlag(perm);
return result;
});
@ -228,5 +200,26 @@ namespace PlexRequests.UI.Helpers
};
}
private Permissions GetPermissions(IUserIdentity user)
{
if (user == null) return 0;
var dbUser = UserRepository.GetUserByUsername(user.UserName);
if (dbUser != null)
{
var permissions = (Permissions)dbUser.Permissions;
return permissions;
}
var plexUser = PlexUsers.GetUserByUsername(user.UserName);
if (plexUser != null)
{
var permissions = (Permissions)plexUser.Permissions;
return permissions;
}
return 0;
}
}
}

View file

@ -63,7 +63,8 @@ namespace PlexRequests.UI.Jobs
{
JobBuilder.Create<PlexAvailabilityChecker>().WithIdentity("PlexAvailabilityChecker", "Plex").Build(),
JobBuilder.Create<PlexContentCacher>().WithIdentity("PlexContentCacher", "Plex").Build(),
JobBuilder.Create<PlexEpisodeCacher>().WithIdentity("PlexEpisodeCacher", "Cache").Build(),
JobBuilder.Create<PlexEpisodeCacher>().WithIdentity("PlexEpisodeCacher", "Plex").Build(),
JobBuilder.Create<PlexUserChecker>().WithIdentity("PlexUserChecker", "Plex").Build(),
JobBuilder.Create<SickRageCacher>().WithIdentity("SickRageCacher", "Cache").Build(),
JobBuilder.Create<SonarrCacher>().WithIdentity("SonarrCacher", "Cache").Build(),
JobBuilder.Create<CouchPotatoCacher>().WithIdentity("CouchPotatoCacher", "Cache").Build(),
@ -159,6 +160,10 @@ namespace PlexRequests.UI.Jobs
{
s.PlexContentCacher = 60;
}
if (s.PlexUserChecker == 0)
{
s.PlexUserChecker = 24;
}
var triggers = new List<ITrigger>();
@ -175,6 +180,13 @@ namespace PlexRequests.UI.Jobs
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexContentCacher).RepeatForever())
.Build();
var plexUserChecker =
TriggerBuilder.Create()
.WithIdentity("PlexUserChecker", "Plex")
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInMinutes(s.PlexUserChecker).RepeatForever())
.Build();
var srCacher =
TriggerBuilder.Create()
.WithIdentity("SickRageCacher", "Cache")
@ -253,6 +265,7 @@ namespace PlexRequests.UI.Jobs
triggers.Add(plexEpCacher);
triggers.Add(fault);
triggers.Add(plexCacher);
triggers.Add(plexUserChecker);
return triggers;
}

View file

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

View file

@ -35,13 +35,14 @@ using PlexRequests.Helpers.Permissions;
using PlexRequests.Store;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules.Admin
{
public class FaultQueueModule : BaseModule
{
public FaultQueueModule(ISettingsService<PlexRequestSettings> settingsService, ICacheProvider cache, IRepository<RequestQueue> requestQueue) : base("admin", settingsService)
public FaultQueueModule(ISettingsService<PlexRequestSettings> settingsService, ICacheProvider cache, IRepository<RequestQueue> requestQueue, ISecurityExtensions security) : base("admin", settingsService, security)
{
Cache = cache;
RequestQueue = requestQueue;

View file

@ -38,13 +38,14 @@ using PlexRequests.Core.SettingModels;
using PlexRequests.Core.StatusChecker;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules.Admin
{
public class SystemStatusModule : BaseModule
{
public SystemStatusModule(ISettingsService<PlexRequestSettings> settingsService, ICacheProvider cache, ISettingsService<SystemSettings> ss) : base("admin", settingsService)
public SystemStatusModule(ISettingsService<PlexRequestSettings> settingsService, ICacheProvider cache, ISettingsService<SystemSettings> ss, ISecurityExtensions security) : base("admin", settingsService, security)
{
Cache = cache;
SystemSettings = ss;

View file

@ -29,12 +29,13 @@ using Nancy.Responses.Negotiation;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.Modules
{
public class ApiDocsModule : BaseModule
{
public ApiDocsModule(ISettingsService<PlexRequestSettings> pr) : base("apidocs", pr)
public ApiDocsModule(ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base("apidocs", pr, security)
{
Get["/"] = x => Documentation();
}

View file

@ -37,13 +37,14 @@ using Newtonsoft.Json;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules
{
public class ApiRequestModule : BaseApiModule
{
public ApiRequestModule(IRequestService service, ISettingsService<PlexRequestSettings> pr) : base("api", pr)
public ApiRequestModule(IRequestService service, ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base("api", pr, security)
{
Get["GetRequests","/requests"] = x => GetRequests();
Get["GetRequest","/requests/{id}"] = x => GetSingleRequests(x);

View file

@ -37,6 +37,7 @@ using Newtonsoft.Json;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.Modules
{
@ -44,7 +45,7 @@ namespace PlexRequests.UI.Modules
{
public ApiSettingsModule(ISettingsService<PlexRequestSettings> pr, ISettingsService<AuthenticationSettings> auth,
ISettingsService<PlexSettings> plexSettings, ISettingsService<CouchPotatoSettings> cp,
ISettingsService<SonarrSettings> sonarr, ISettingsService<SickRageSettings> sr, ISettingsService<HeadphonesSettings> hp) : base("api", pr)
ISettingsService<SonarrSettings> sonarr, ISettingsService<SickRageSettings> sr, ISettingsService<HeadphonesSettings> hp, ISecurityExtensions security) : base("api", pr, security)
{
Get["GetVersion", "/version"] = x => GetVersion();

View file

@ -33,13 +33,14 @@ using Nancy.ModelBinding;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules
{
public class ApiUserModule : BaseApiModule
{
public ApiUserModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m) : base("api", pr)
public ApiUserModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, ISecurityExtensions security) : base("api", pr, security)
{
Put["PutCredentials", "/credentials/{username}"] = x => ChangePassword(x);

View file

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

View file

@ -52,7 +52,8 @@ namespace PlexRequests.UI.Modules
public ApprovalModule(IRequestService service, ISettingsService<CouchPotatoSettings> cpService, ICouchPotatoApi cpApi, ISonarrApi sonarrApi,
ISettingsService<SonarrSettings> sonarrSettings, ISickRageApi srApi, ISettingsService<SickRageSettings> srSettings,
ISettingsService<HeadphonesSettings> hpSettings, IHeadphonesApi hpApi, ISettingsService<PlexRequestSettings> pr, ITransientFaultQueue faultQueue) : base("approval", pr)
ISettingsService<HeadphonesSettings> hpSettings, IHeadphonesApi hpApi, ISettingsService<PlexRequestSettings> pr, ITransientFaultQueue faultQueue
, ISecurityExtensions security) : base("approval", pr, security)
{
this.RequiresAnyClaim(UserClaims.Admin, UserClaims.PowerUser);
@ -68,6 +69,7 @@ namespace PlexRequests.UI.Modules
SickRageSettings = srSettings;
HeadphonesSettings = hpSettings;
HeadphoneApi = hpApi;
FaultQueue = faultQueue;
Post["/approve", true] = async (x, ct) => await Approve((int)Request.Form.requestid, (string)Request.Form.qualityId);
Post["/deny", true] = async (x, ct) => await DenyRequest((int)Request.Form.requestid, (string)Request.Form.reason);

View file

@ -33,18 +33,19 @@ using Nancy.Validation;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Store;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.Modules
{
public abstract class BaseApiModule : BaseModule
{
protected BaseApiModule(ISettingsService<PlexRequestSettings> s) : base(s)
protected BaseApiModule(ISettingsService<PlexRequestSettings> s, ISecurityExtensions security) : base(s,security)
{
Settings = s;
Before += (ctx) => CheckAuth();
}
protected BaseApiModule(string modulePath, ISettingsService<PlexRequestSettings> s) : base(modulePath, s)
protected BaseApiModule(string modulePath, ISettingsService<PlexRequestSettings> s, ISecurityExtensions security) : base(modulePath, s, security)
{
Settings = s;
Before += (ctx) => CheckAuth();

View file

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

View file

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

View file

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

View file

@ -7,12 +7,13 @@ using NLog;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.Modules
{
public class DonationLinkModule : BaseAuthModule
{
public DonationLinkModule(ICacheProvider provider, ISettingsService<PlexRequestSettings> pr) : base("customDonation", pr)
public DonationLinkModule(ICacheProvider provider, ISettingsService<PlexRequestSettings> pr, ISecurityExtensions security) : base("customDonation", pr, security)
{
Cache = provider;

View file

@ -33,12 +33,13 @@ using Nancy.Responses;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.Modules
{
public class IndexModule : BaseAuthModule
{
public IndexModule(ISettingsService<PlexRequestSettings> pr, ISettingsService<LandingPageSettings> l, IResourceLinker rl) : base(pr)
public IndexModule(ISettingsService<PlexRequestSettings> pr, ISettingsService<LandingPageSettings> l, IResourceLinker rl, ISecurityExtensions security) : base(pr, security)
{
LandingPage = l;
Linker = rl;

View file

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

View file

@ -33,6 +33,7 @@ using Nancy.Linker;
using PlexRequests.Api.Interfaces;
using PlexRequests.Core;
using PlexRequests.Core.SettingModels;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules
@ -40,7 +41,7 @@ namespace PlexRequests.UI.Modules
public class LandingPageModule : BaseModule
{
public LandingPageModule(ISettingsService<PlexRequestSettings> settingsService, ISettingsService<LandingPageSettings> landing,
ISettingsService<PlexSettings> ps, IPlexApi pApi, IResourceLinker linker) : base("landing", settingsService)
ISettingsService<PlexSettings> ps, IPlexApi pApi, IResourceLinker linker, ISecurityExtensions security) : base("landing", settingsService, security)
{
LandingSettings = landing;
PlexSettings = ps;

View file

@ -38,13 +38,14 @@ using PlexRequests.Core.StatusChecker;
using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Jobs;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules
{
public class LayoutModule : BaseAuthModule
{
public LayoutModule(ICacheProvider provider, ISettingsService<PlexRequestSettings> pr, ISettingsService<SystemSettings> settings, IJobRecord rec) : base("layout", pr)
public LayoutModule(ICacheProvider provider, ISettingsService<PlexRequestSettings> pr, ISettingsService<SystemSettings> settings, IJobRecord rec, ISecurityExtensions security) : base("layout", pr, security)
{
Cache = provider;
SystemSettings = settings;

View file

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

View file

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

View file

@ -48,6 +48,7 @@ using NLog;
using PlexRequests.Core.Models;
using PlexRequests.Helpers.Analytics;
using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Helpers;
using Action = PlexRequests.Helpers.Analytics.Action;
namespace PlexRequests.UI.Modules
@ -67,7 +68,8 @@ namespace PlexRequests.UI.Modules
ISickRageApi sickRageApi,
ICacheProvider cache,
IAnalytics an,
INotificationEngine engine) : base("requests", prSettings)
INotificationEngine engine,
ISecurityExtensions security) : base("requests", prSettings, security)
{
Service = service;
PrSettings = prSettings;

View file

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

View file

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

View file

@ -17,13 +17,15 @@ using PlexRequests.Helpers.Permissions;
using PlexRequests.Store;
using PlexRequests.Store.Models;
using PlexRequests.Store.Repository;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
namespace PlexRequests.UI.Modules
{
public class UserManagementModule : BaseModule
{
public UserManagementModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService<PlexSettings> plex, IRepository<UserLogins> userLogins, IRepository<PlexUsers> plexRepo) : base("usermanagement", pr)
public UserManagementModule(ISettingsService<PlexRequestSettings> pr, ICustomUserMapper m, IPlexApi plexApi, ISettingsService<PlexSettings> plex, IRepository<UserLogins> userLogins, IPlexUserRepository plexRepo
, ISecurityExtensions security) : base("usermanagement", pr, security)
{
#if !DEBUG
Before += (ctx) => Security.AdminLoginRedirect(Permissions.Administrator, ctx);
@ -51,7 +53,7 @@ namespace PlexRequests.UI.Modules
private IPlexApi PlexApi { get; }
private ISettingsService<PlexSettings> PlexSettings { get; }
private IRepository<UserLogins> UserLoginsRepo { get; }
private IRepository<PlexUsers> PlexUsersRepository { get; }
private IPlexUserRepository PlexUsersRepository { get; }
private ISettingsService<PlexRequestSettings> PlexRequestSettings { get; }
private Negotiator Load()
@ -112,11 +114,21 @@ namespace PlexRequests.UI.Modules
{
return Response.AsJson(new JsonResponseModel
{
Result = true,
Result = false,
Message = "Please enter in a valid Username and Password"
});
}
var users = UserMapper.GetUsers();
if (users.Any(x => x.UserName.Equals(model.Username, StringComparison.CurrentCultureIgnoreCase)))
{
return Response.AsJson(new JsonResponseModel
{
Result = false,
Message = $"A user with the username '{model.Username}' already exists"
});
}
var featuresVal = 0;
var permissionsVal = 0;
@ -213,7 +225,10 @@ namespace PlexRequests.UI.Modules
Permissions = permissionsValue,
Features = featuresValue,
UserAlias = model.Alias,
PlexUserId = plexUser.Id
PlexUserId = plexUser.Id,
EmailAddress = plexUser.Email,
Username = plexUser.Username,
LoginId = Guid.NewGuid().ToString()
};
await PlexUsersRepository.InsertAsync(user);

View file

@ -41,6 +41,7 @@ using PlexRequests.Core.SettingModels;
using PlexRequests.Helpers;
using PlexRequests.Helpers.Analytics;
using PlexRequests.Helpers.Permissions;
using PlexRequests.UI.Authentication;
using PlexRequests.UI.Helpers;
using PlexRequests.UI.Models;
@ -51,7 +52,7 @@ namespace PlexRequests.UI.Modules
public class UserWizardModule : BaseModule
{
public UserWizardModule(ISettingsService<PlexRequestSettings> pr, ISettingsService<PlexSettings> plex, IPlexApi plexApi,
ISettingsService<AuthenticationSettings> auth, ICustomUserMapper m, IAnalytics a) : base("wizard", pr)
ISettingsService<AuthenticationSettings> auth, ICustomUserMapper m, IAnalytics a, ISecurityExtensions security) : base("wizard", pr, security)
{
PlexSettings = plex;
PlexApi = plexApi;
@ -200,7 +201,7 @@ namespace PlexRequests.UI.Modules
var baseUrl = string.IsNullOrEmpty(settings.BaseUrl) ? string.Empty : $"/{settings.BaseUrl}";
return this.LoginAndRedirect((Guid)userId, fallbackRedirectUrl: $"{baseUrl}/search");
return CustomModuleExtensions.LoginAndRedirect(this,(Guid)userId, fallbackRedirectUrl: $"{baseUrl}/search");
}
}
}

View file

@ -38,6 +38,7 @@ using PlexRequests.Helpers;
using PlexRequests.Services.Interfaces;
using PlexRequests.Services.Notification;
using PlexRequests.Store;
using PlexRequests.UI.Helpers;
namespace PlexRequests.UI.NinjectModules
{
@ -59,6 +60,8 @@ namespace PlexRequests.UI.NinjectModules
Bind<INotificationEngine>().To<NotificationEngine>();
Bind<IStatusChecker>().To<StatusChecker>();
Bind<ISecurityExtensions>().To<SecurityExtensions>();
}
}
}

View file

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

View file

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

View file

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