mirror of
https://github.com/Radarr/Radarr.git
synced 2024-11-09 04:22:30 +01:00
New: Forms authentication
This commit is contained in:
parent
7c38fcb9f3
commit
3c756348eb
@ -9,5 +9,5 @@ require('./copy');
|
|||||||
|
|
||||||
gulp.task('build', function () {
|
gulp.task('build', function () {
|
||||||
return runSequence('clean',
|
return runSequence('clean',
|
||||||
['requireJs', 'less', 'handlebars', 'copyIndex', 'copyContent']);
|
['requireJs', 'less', 'handlebars', 'copyHtml', 'copyContent']);
|
||||||
});
|
});
|
@ -11,9 +11,9 @@ gulp.task('copyJs', function () {
|
|||||||
.pipe(gulp.dest(paths.dest.root));
|
.pipe(gulp.dest(paths.dest.root));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('copyIndex', function () {
|
gulp.task('copyHtml', function () {
|
||||||
return gulp.src(paths.src.index)
|
return gulp.src(paths.src.html)
|
||||||
.pipe(cache('copyIndex'))
|
.pipe(cache('copyHtml'))
|
||||||
.pipe(gulp.dest(paths.dest.root));
|
.pipe(gulp.dest(paths.dest.root));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ module.exports = {
|
|||||||
src: {
|
src: {
|
||||||
root: './src/UI/',
|
root: './src/UI/',
|
||||||
templates: './src/UI/**/*.hbs',
|
templates: './src/UI/**/*.hbs',
|
||||||
index: './src/UI/index.html',
|
html: './src/UI/*.html',
|
||||||
partials: './src/UI/**/*Partial.hbs',
|
partials: './src/UI/**/*Partial.hbs',
|
||||||
scripts: './src/UI/**/*.js',
|
scripts: './src/UI/**/*.js',
|
||||||
less: ['./src/UI/**/*.less'],
|
less: ['./src/UI/**/*.less'],
|
||||||
|
@ -10,11 +10,11 @@ require('./less.js');
|
|||||||
require('./copy.js');
|
require('./copy.js');
|
||||||
|
|
||||||
|
|
||||||
gulp.task('watch', ['jshint', 'handlebars', 'less', 'copyJs','copyIndex', 'copyContent'], function () {
|
gulp.task('watch', ['jshint', 'handlebars', 'less', 'copyJs', 'copyHtml', 'copyContent'], function () {
|
||||||
gulp.watch([paths.src.scripts, paths.src.exclude.libs], ['jshint', 'copyJs']);
|
gulp.watch([paths.src.scripts, paths.src.exclude.libs], ['jshint', 'copyJs']);
|
||||||
gulp.watch(paths.src.templates, ['handlebars']);
|
gulp.watch(paths.src.templates, ['handlebars']);
|
||||||
gulp.watch([paths.src.less, paths.src.exclude.libs], ['less']);
|
gulp.watch([paths.src.less, paths.src.exclude.libs], ['less']);
|
||||||
gulp.watch([paths.src.index], ['copyIndex']);
|
gulp.watch([paths.src.html], ['copyHtml']);
|
||||||
gulp.watch([paths.src.content + '**/*.*', '!**/*.less'], ['copyContent']);
|
gulp.watch([paths.src.content + '**/*.*', '!**/*.less'], ['copyContent']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -23,7 +23,8 @@ gulp.task('liveReload', ['jshint', 'handlebars', 'less', 'copyJs'], function ()
|
|||||||
gulp.watch([
|
gulp.watch([
|
||||||
'app/**/*.js',
|
'app/**/*.js',
|
||||||
'app/**/*.css',
|
'app/**/*.css',
|
||||||
'app/index.html'
|
'app/index.html',
|
||||||
|
'app/login.html'
|
||||||
]).on('change', function (file) {
|
]).on('change', function (file) {
|
||||||
server.changed(file.path);
|
server.changed(file.path);
|
||||||
});
|
});
|
||||||
|
62
src/NzbDrone.Api/Authentication/1tews5g3.gd1~
Normal file
62
src/NzbDrone.Api/Authentication/1tews5g3.gd1~
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using Nancy;
|
||||||
|
using Nancy.Authentication.Basic;
|
||||||
|
using Nancy.Authentication.Forms;
|
||||||
|
using Nancy.Bootstrapper;
|
||||||
|
using Nancy.Cryptography;
|
||||||
|
using NzbDrone.Api.Extensions.Pipelines;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
|
namespace NzbDrone.Api.Authentication
|
||||||
|
{
|
||||||
|
public class EnableAuthInNancy : IRegisterNancyPipeline
|
||||||
|
{
|
||||||
|
private readonly IAuthenticationService _authenticationService;
|
||||||
|
private readonly IConfigService _configService;
|
||||||
|
private readonly IConfigFileProvider _configFileProvider;
|
||||||
|
|
||||||
|
public EnableAuthInNancy(IAuthenticationService authenticationService,
|
||||||
|
IConfigService configService,
|
||||||
|
IConfigFileProvider configFileProvider)
|
||||||
|
{
|
||||||
|
_authenticationService = authenticationService;
|
||||||
|
_configService = configService;
|
||||||
|
_configFileProvider = configFileProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Register(IPipelines pipelines)
|
||||||
|
{
|
||||||
|
RegisterFormsAuth(pipelines);
|
||||||
|
pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Sonarr"));
|
||||||
|
pipelines.BeforeRequest.AddItemToEndOfPipeline(RequiresAuthentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response RequiresAuthentication(NancyContext context)
|
||||||
|
{
|
||||||
|
Response response = null;
|
||||||
|
|
||||||
|
if (!_authenticationService.IsAuthenticated(context))
|
||||||
|
{
|
||||||
|
response = new Response { StatusCode = HttpStatusCode.Unauthorized };
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterFormsAuth(IPipelines pipelines)
|
||||||
|
{
|
||||||
|
var cryptographyConfiguration = new CryptographyConfiguration(
|
||||||
|
new RijndaelEncryptionProvider(new PassphraseKeyGenerator(_configService.RijndaelPassphrase,
|
||||||
|
new byte[] {1, 2, 3, 4, 5, 6, 7, 8})),
|
||||||
|
new DefaultHmacProvider(new PassphraseKeyGenerator(_configService.HmacPassphrase,
|
||||||
|
new byte[] {1, 2, 3, 4, 5, 6, 7, 8}))
|
||||||
|
);
|
||||||
|
|
||||||
|
FormsAuthentication.Enable(pipelines, new FormsAuthenticationConfiguration
|
||||||
|
{
|
||||||
|
RedirectUrl = "~/login",
|
||||||
|
UserMapper = _authenticationService,
|
||||||
|
CryptographyConfiguration = cryptographyConfiguration
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,14 +2,16 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Nancy;
|
using Nancy;
|
||||||
using Nancy.Authentication.Basic;
|
using Nancy.Authentication.Basic;
|
||||||
|
using Nancy.Authentication.Forms;
|
||||||
using Nancy.Security;
|
using Nancy.Security;
|
||||||
using NzbDrone.Api.Extensions;
|
using NzbDrone.Api.Extensions;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Authentication;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
namespace NzbDrone.Api.Authentication
|
namespace NzbDrone.Api.Authentication
|
||||||
{
|
{
|
||||||
public interface IAuthenticationService : IUserValidator
|
public interface IAuthenticationService : IUserValidator, IUserMapper
|
||||||
{
|
{
|
||||||
bool IsAuthenticated(NancyContext context);
|
bool IsAuthenticated(NancyContext context);
|
||||||
}
|
}
|
||||||
@ -17,37 +19,49 @@ public interface IAuthenticationService : IUserValidator
|
|||||||
public class AuthenticationService : IAuthenticationService
|
public class AuthenticationService : IAuthenticationService
|
||||||
{
|
{
|
||||||
private readonly IConfigFileProvider _configFileProvider;
|
private readonly IConfigFileProvider _configFileProvider;
|
||||||
|
private readonly IUserService _userService;
|
||||||
private static readonly NzbDroneUser AnonymousUser = new NzbDroneUser { UserName = "Anonymous" };
|
private static readonly NzbDroneUser AnonymousUser = new NzbDroneUser { UserName = "Anonymous" };
|
||||||
private static String API_KEY;
|
private static String API_KEY;
|
||||||
|
|
||||||
public AuthenticationService(IConfigFileProvider configFileProvider)
|
public AuthenticationService(IConfigFileProvider configFileProvider, IUserService userService)
|
||||||
{
|
{
|
||||||
_configFileProvider = configFileProvider;
|
_configFileProvider = configFileProvider;
|
||||||
|
_userService = userService;
|
||||||
API_KEY = configFileProvider.ApiKey;
|
API_KEY = configFileProvider.ApiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IUserIdentity Validate(string username, string password)
|
public IUserIdentity Validate(string username, string password)
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
if (_configFileProvider.AuthenticationMethod == AuthenticationType.None)
|
||||||
{
|
{
|
||||||
return AnonymousUser;
|
return AnonymousUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_configFileProvider.Username.Equals(username) &&
|
var user = _userService.FindUser(username, password);
|
||||||
_configFileProvider.Password.Equals(password))
|
|
||||||
|
if (user != null)
|
||||||
{
|
{
|
||||||
return new NzbDroneUser { UserName = username };
|
return new NzbDroneUser { UserName = user.Username };
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool Enabled
|
public IUserIdentity GetUserFromIdentifier(Guid identifier, NancyContext context)
|
||||||
{
|
{
|
||||||
get
|
if (_configFileProvider.AuthenticationMethod == AuthenticationType.None)
|
||||||
{
|
{
|
||||||
return _configFileProvider.AuthenticationEnabled;
|
return AnonymousUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var user = _userService.FindUser(identifier);
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
return new NzbDroneUser { UserName = user.Username };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsAuthenticated(NancyContext context)
|
public bool IsAuthenticated(NancyContext context)
|
||||||
@ -59,13 +73,13 @@ public bool IsAuthenticated(NancyContext context)
|
|||||||
return ValidApiKey(apiKey);
|
return ValidApiKey(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.Request.IsFeedRequest())
|
if (_configFileProvider.AuthenticationMethod == AuthenticationType.None)
|
||||||
{
|
|
||||||
if (!Enabled)
|
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (context.Request.IsFeedRequest())
|
||||||
|
{
|
||||||
if (ValidUser(context) || ValidApiKey(apiKey))
|
if (ValidUser(context) || ValidApiKey(apiKey))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
@ -74,7 +88,12 @@ public bool IsAuthenticated(NancyContext context)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Enabled)
|
if (context.Request.IsLoginRequest())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.Request.IsContentRequest())
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,46 @@
|
|||||||
using Nancy;
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using Nancy;
|
||||||
using Nancy.Authentication.Basic;
|
using Nancy.Authentication.Basic;
|
||||||
|
using Nancy.Authentication.Forms;
|
||||||
using Nancy.Bootstrapper;
|
using Nancy.Bootstrapper;
|
||||||
|
using Nancy.Cryptography;
|
||||||
|
using NzbDrone.Api.Extensions;
|
||||||
using NzbDrone.Api.Extensions.Pipelines;
|
using NzbDrone.Api.Extensions.Pipelines;
|
||||||
|
using NzbDrone.Core.Authentication;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
namespace NzbDrone.Api.Authentication
|
namespace NzbDrone.Api.Authentication
|
||||||
{
|
{
|
||||||
public class EnableAuthInNancy : IRegisterNancyPipeline
|
public class EnableAuthInNancy : IRegisterNancyPipeline
|
||||||
{
|
{
|
||||||
private readonly IAuthenticationService _authenticationService;
|
private readonly IAuthenticationService _authenticationService;
|
||||||
|
private readonly IConfigService _configService;
|
||||||
|
private readonly IConfigFileProvider _configFileProvider;
|
||||||
|
|
||||||
public EnableAuthInNancy(IAuthenticationService authenticationService)
|
public EnableAuthInNancy(IAuthenticationService authenticationService,
|
||||||
|
IConfigService configService,
|
||||||
|
IConfigFileProvider configFileProvider)
|
||||||
{
|
{
|
||||||
_authenticationService = authenticationService;
|
_authenticationService = authenticationService;
|
||||||
|
_configService = configService;
|
||||||
|
_configFileProvider = configFileProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Register(IPipelines pipelines)
|
public void Register(IPipelines pipelines)
|
||||||
|
{
|
||||||
|
if (_configFileProvider.AuthenticationMethod == AuthenticationType.Forms)
|
||||||
|
{
|
||||||
|
RegisterFormsAuth(pipelines);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (_configFileProvider.AuthenticationMethod == AuthenticationType.Basic)
|
||||||
{
|
{
|
||||||
pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Sonarr"));
|
pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Sonarr"));
|
||||||
|
}
|
||||||
|
|
||||||
pipelines.BeforeRequest.AddItemToEndOfPipeline(RequiresAuthentication);
|
pipelines.BeforeRequest.AddItemToEndOfPipeline(RequiresAuthentication);
|
||||||
|
pipelines.AfterRequest.AddItemToEndOfPipeline(RemoveLoginHooksForApiCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response RequiresAuthentication(NancyContext context)
|
private Response RequiresAuthentication(NancyContext context)
|
||||||
@ -31,5 +54,33 @@ private Response RequiresAuthentication(NancyContext context)
|
|||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RegisterFormsAuth(IPipelines pipelines)
|
||||||
|
{
|
||||||
|
var cryptographyConfiguration = new CryptographyConfiguration(
|
||||||
|
new RijndaelEncryptionProvider(new PassphraseKeyGenerator(_configService.RijndaelPassphrase, Encoding.ASCII.GetBytes(_configService.RijndaelSalt))),
|
||||||
|
new DefaultHmacProvider(new PassphraseKeyGenerator(_configService.HmacPassphrase, Encoding.ASCII.GetBytes(_configService.HmacSalt)))
|
||||||
|
);
|
||||||
|
|
||||||
|
FormsAuthentication.Enable(pipelines, new FormsAuthenticationConfiguration
|
||||||
|
{
|
||||||
|
RedirectUrl = "~/login",
|
||||||
|
UserMapper = _authenticationService,
|
||||||
|
CryptographyConfiguration = cryptographyConfiguration
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveLoginHooksForApiCalls(NancyContext context)
|
||||||
|
{
|
||||||
|
if (context.Request.IsApiRequest())
|
||||||
|
{
|
||||||
|
if ((context.Response.StatusCode == HttpStatusCode.SeeOther &&
|
||||||
|
context.Response.ContentType.Equals("text/html", StringComparison.InvariantCultureIgnoreCase)) ||
|
||||||
|
context.Response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
context.Response = new { Error = "Unauthorized" }.AsResponse(HttpStatusCode.Unauthorized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
39
src/NzbDrone.Api/Authentication/LoginModule.cs
Normal file
39
src/NzbDrone.Api/Authentication/LoginModule.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using Nancy;
|
||||||
|
using Nancy.Authentication.Forms;
|
||||||
|
using Nancy.Extensions;
|
||||||
|
using Nancy.ModelBinding;
|
||||||
|
using NzbDrone.Core.Authentication;
|
||||||
|
|
||||||
|
namespace NzbDrone.Api.Authentication
|
||||||
|
{
|
||||||
|
public class LoginModule : NancyModule
|
||||||
|
{
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
|
||||||
|
public LoginModule(IUserService userService)
|
||||||
|
{
|
||||||
|
_userService = userService;
|
||||||
|
Post["/login"] = x => Login(this.Bind<LoginResource>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response Login(LoginResource resource)
|
||||||
|
{
|
||||||
|
var user = _userService.FindUser(resource.Username, resource.Password);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return Context.GetRedirect("~/login?returnUrl=" + (string)Request.Query.returnUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime? expiry = null;
|
||||||
|
|
||||||
|
if (resource.RememberMe)
|
||||||
|
{
|
||||||
|
expiry = DateTime.UtcNow.AddDays(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.LoginAndRedirect(user.Identifier, expiry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
src/NzbDrone.Api/Authentication/LoginResource.cs
Normal file
9
src/NzbDrone.Api/Authentication/LoginResource.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace NzbDrone.Api.Authentication
|
||||||
|
{
|
||||||
|
public class LoginResource
|
||||||
|
{
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
public bool RememberMe { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,8 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Authentication;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Update;
|
using NzbDrone.Core.Update;
|
||||||
using NzbDrone.Core.Validation;
|
using NzbDrone.Core.Validation;
|
||||||
@ -13,11 +15,13 @@ namespace NzbDrone.Api.Config
|
|||||||
public class HostConfigModule : NzbDroneRestModule<HostConfigResource>
|
public class HostConfigModule : NzbDroneRestModule<HostConfigResource>
|
||||||
{
|
{
|
||||||
private readonly IConfigFileProvider _configFileProvider;
|
private readonly IConfigFileProvider _configFileProvider;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
|
||||||
public HostConfigModule(IConfigFileProvider configFileProvider)
|
public HostConfigModule(IConfigFileProvider configFileProvider, IUserService userService)
|
||||||
: base("/config/host")
|
: base("/config/host")
|
||||||
{
|
{
|
||||||
_configFileProvider = configFileProvider;
|
_configFileProvider = configFileProvider;
|
||||||
|
_userService = userService;
|
||||||
|
|
||||||
GetResourceSingle = GetHostConfig;
|
GetResourceSingle = GetHostConfig;
|
||||||
GetResourceById = GetHostConfig;
|
GetResourceById = GetHostConfig;
|
||||||
@ -26,8 +30,8 @@ public HostConfigModule(IConfigFileProvider configFileProvider)
|
|||||||
SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default");
|
SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default");
|
||||||
SharedValidator.RuleFor(c => c.Port).ValidPort();
|
SharedValidator.RuleFor(c => c.Port).ValidPort();
|
||||||
|
|
||||||
SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationEnabled);
|
SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None);
|
||||||
SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationEnabled);
|
SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None);
|
||||||
|
|
||||||
SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl);
|
SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl);
|
||||||
SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl && OsInfo.IsWindows);
|
SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl && OsInfo.IsWindows);
|
||||||
@ -46,6 +50,14 @@ private HostConfigResource GetHostConfig()
|
|||||||
resource.InjectFrom(_configFileProvider);
|
resource.InjectFrom(_configFileProvider);
|
||||||
resource.Id = 1;
|
resource.Id = 1;
|
||||||
|
|
||||||
|
var user = _userService.FindUser();
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
resource.Username = user.Username;
|
||||||
|
resource.Password = user.Password;
|
||||||
|
}
|
||||||
|
|
||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +73,11 @@ private void SaveHostConfig(HostConfigResource resource)
|
|||||||
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
|
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
|
||||||
|
|
||||||
_configFileProvider.SaveConfigDictionary(dictionary);
|
_configFileProvider.SaveConfigDictionary(dictionary);
|
||||||
|
|
||||||
|
if (resource.Username.IsNotNullOrWhiteSpace() && resource.Password.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
_userService.Upsert(resource.Username, resource.Password);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using NzbDrone.Api.REST;
|
using NzbDrone.Api.REST;
|
||||||
|
using NzbDrone.Core.Authentication;
|
||||||
using NzbDrone.Core.Update;
|
using NzbDrone.Core.Update;
|
||||||
|
|
||||||
namespace NzbDrone.Api.Config
|
namespace NzbDrone.Api.Config
|
||||||
@ -11,7 +12,7 @@ public class HostConfigResource : RestResource
|
|||||||
public Int32 SslPort { get; set; }
|
public Int32 SslPort { get; set; }
|
||||||
public Boolean EnableSsl { get; set; }
|
public Boolean EnableSsl { get; set; }
|
||||||
public Boolean LaunchBrowser { get; set; }
|
public Boolean LaunchBrowser { get; set; }
|
||||||
public bool AuthenticationEnabled { get; set; }
|
public AuthenticationType AuthenticationMethod { get; set; }
|
||||||
public Boolean AnalyticsEnabled { get; set; }
|
public Boolean AnalyticsEnabled { get; set; }
|
||||||
public String Username { get; set; }
|
public String Username { get; set; }
|
||||||
public String Password { get; set; }
|
public String Password { get; set; }
|
||||||
|
@ -26,5 +26,15 @@ public static bool IsLocalRequest(this Request request)
|
|||||||
request.UserHostAddress.Equals("127.0.0.1") ||
|
request.UserHostAddress.Equals("127.0.0.1") ||
|
||||||
request.UserHostAddress.Equals("::1"));
|
request.UserHostAddress.Equals("::1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsLoginRequest(this Request request)
|
||||||
|
{
|
||||||
|
return request.Path.Equals("/login", StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsContentRequest(this Request request)
|
||||||
|
{
|
||||||
|
return request.Path.StartsWith("/Content/", StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ public override string Map(string resourceUrl)
|
|||||||
|
|
||||||
public override bool CanHandle(string resourceUrl)
|
public override bool CanHandle(string resourceUrl)
|
||||||
{
|
{
|
||||||
return !resourceUrl.Contains(".");
|
return !resourceUrl.Contains(".") && !resourceUrl.StartsWith("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Response GetResponse(string resourceUrl)
|
public override Response GetResponse(string resourceUrl)
|
||||||
|
88
src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs
Normal file
88
src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Nancy;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
|
namespace NzbDrone.Api.Frontend.Mappers
|
||||||
|
{
|
||||||
|
public class LoginHtmlMapper : StaticResourceMapperBase
|
||||||
|
{
|
||||||
|
private readonly IDiskProvider _diskProvider;
|
||||||
|
private readonly Func<ICacheBreakerProvider> _cacheBreakProviderFactory;
|
||||||
|
private readonly string _indexPath;
|
||||||
|
private static readonly Regex ReplaceRegex = new Regex("(?<=(?:href|src|data-main)=\").*?(?=\")", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
private static String URL_BASE;
|
||||||
|
private string _generatedContent;
|
||||||
|
|
||||||
|
public LoginHtmlMapper(IAppFolderInfo appFolderInfo,
|
||||||
|
IDiskProvider diskProvider,
|
||||||
|
IConfigFileProvider configFileProvider,
|
||||||
|
Func<ICacheBreakerProvider> cacheBreakProviderFactory,
|
||||||
|
Logger logger)
|
||||||
|
: base(diskProvider, logger)
|
||||||
|
{
|
||||||
|
_diskProvider = diskProvider;
|
||||||
|
_cacheBreakProviderFactory = cacheBreakProviderFactory;
|
||||||
|
_indexPath = Path.Combine(appFolderInfo.StartUpFolder, "UI", "login.html");
|
||||||
|
|
||||||
|
URL_BASE = configFileProvider.UrlBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Map(string resourceUrl)
|
||||||
|
{
|
||||||
|
return _indexPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanHandle(string resourceUrl)
|
||||||
|
{
|
||||||
|
return resourceUrl.StartsWith("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Response GetResponse(string resourceUrl)
|
||||||
|
{
|
||||||
|
var response = base.GetResponse(resourceUrl);
|
||||||
|
response.Headers["X-UA-Compatible"] = "IE=edge";
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Stream GetContentStream(string filePath)
|
||||||
|
{
|
||||||
|
var text = GetLoginText();
|
||||||
|
|
||||||
|
var stream = new MemoryStream();
|
||||||
|
var writer = new StreamWriter(stream);
|
||||||
|
writer.Write(text);
|
||||||
|
writer.Flush();
|
||||||
|
stream.Position = 0;
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetLoginText()
|
||||||
|
{
|
||||||
|
if (RuntimeInfoBase.IsProduction && _generatedContent != null)
|
||||||
|
{
|
||||||
|
return _generatedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = _diskProvider.ReadAllText(_indexPath);
|
||||||
|
|
||||||
|
var cacheBreakProvider = _cacheBreakProviderFactory();
|
||||||
|
|
||||||
|
text = ReplaceRegex.Replace(text, match =>
|
||||||
|
{
|
||||||
|
var url = cacheBreakProvider.AddCacheBreakerToPath(match.Value);
|
||||||
|
return URL_BASE + url;
|
||||||
|
});
|
||||||
|
|
||||||
|
_generatedContent = text;
|
||||||
|
|
||||||
|
return _generatedContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -52,6 +52,9 @@
|
|||||||
<SpecificVersion>False</SpecificVersion>
|
<SpecificVersion>False</SpecificVersion>
|
||||||
<HintPath>..\packages\Nancy.Authentication.Basic.0.23.2\lib\net40\Nancy.Authentication.Basic.dll</HintPath>
|
<HintPath>..\packages\Nancy.Authentication.Basic.0.23.2\lib\net40\Nancy.Authentication.Basic.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<Reference Include="Nancy.Authentication.Forms">
|
||||||
|
<HintPath>..\packages\Nancy.Authentication.Forms.0.23.2\lib\net40\Nancy.Authentication.Forms.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
<Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||||
<SpecificVersion>False</SpecificVersion>
|
<SpecificVersion>False</SpecificVersion>
|
||||||
<HintPath>..\packages\Newtonsoft.Json.6.0.6\lib\net40\Newtonsoft.Json.dll</HintPath>
|
<HintPath>..\packages\Newtonsoft.Json.6.0.6\lib\net40\Newtonsoft.Json.dll</HintPath>
|
||||||
@ -80,6 +83,8 @@
|
|||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Authentication\AuthenticationService.cs" />
|
<Compile Include="Authentication\AuthenticationService.cs" />
|
||||||
<Compile Include="Authentication\EnableAuthInNancy.cs" />
|
<Compile Include="Authentication\EnableAuthInNancy.cs" />
|
||||||
|
<Compile Include="Authentication\LoginModule.cs" />
|
||||||
|
<Compile Include="Authentication\LoginResource.cs" />
|
||||||
<Compile Include="Authentication\NzbDroneUser.cs" />
|
<Compile Include="Authentication\NzbDroneUser.cs" />
|
||||||
<Compile Include="Blacklist\BlacklistModule.cs" />
|
<Compile Include="Blacklist\BlacklistModule.cs" />
|
||||||
<Compile Include="Blacklist\BlacklistResource.cs" />
|
<Compile Include="Blacklist\BlacklistResource.cs" />
|
||||||
@ -94,6 +99,7 @@
|
|||||||
<Compile Include="Commands\CommandResource.cs" />
|
<Compile Include="Commands\CommandResource.cs" />
|
||||||
<Compile Include="Extensions\AccessControlHeaders.cs" />
|
<Compile Include="Extensions\AccessControlHeaders.cs" />
|
||||||
<Compile Include="Extensions\Pipelines\CorsPipeline.cs" />
|
<Compile Include="Extensions\Pipelines\CorsPipeline.cs" />
|
||||||
|
<Compile Include="Frontend\Mappers\LoginHtmlMapper.cs" />
|
||||||
<Compile Include="Profiles\Delay\DelayProfileModule.cs" />
|
<Compile Include="Profiles\Delay\DelayProfileModule.cs" />
|
||||||
<Compile Include="Profiles\Delay\DelayProfileResource.cs" />
|
<Compile Include="Profiles\Delay\DelayProfileResource.cs" />
|
||||||
<Compile Include="Profiles\Delay\DelayProfileValidator.cs" />
|
<Compile Include="Profiles\Delay\DelayProfileValidator.cs" />
|
||||||
|
@ -57,7 +57,7 @@ private Response GetStatus()
|
|||||||
IsOsx = OsInfo.IsOsx,
|
IsOsx = OsInfo.IsOsx,
|
||||||
IsWindows = OsInfo.IsWindows,
|
IsWindows = OsInfo.IsWindows,
|
||||||
Branch = _configFileProvider.Branch,
|
Branch = _configFileProvider.Branch,
|
||||||
Authentication = _configFileProvider.AuthenticationEnabled,
|
Authentication = _configFileProvider.AuthenticationMethod,
|
||||||
SqliteVersion = _database.Version,
|
SqliteVersion = _database.Version,
|
||||||
UrlBase = _configFileProvider.UrlBase,
|
UrlBase = _configFileProvider.UrlBase,
|
||||||
RuntimeVersion = _runtimeInfo.RuntimeVersion
|
RuntimeVersion = _runtimeInfo.RuntimeVersion
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
<package id="FluentValidation" version="5.5.0.0" targetFramework="net40" />
|
<package id="FluentValidation" version="5.5.0.0" targetFramework="net40" />
|
||||||
<package id="Nancy" version="0.23.2" targetFramework="net40" />
|
<package id="Nancy" version="0.23.2" targetFramework="net40" />
|
||||||
<package id="Nancy.Authentication.Basic" version="0.23.2" targetFramework="net40" />
|
<package id="Nancy.Authentication.Basic" version="0.23.2" targetFramework="net40" />
|
||||||
|
<package id="Nancy.Authentication.Forms" version="0.23.2" targetFramework="net40" />
|
||||||
<package id="Newtonsoft.Json" version="6.0.6" targetFramework="net40" />
|
<package id="Newtonsoft.Json" version="6.0.6" targetFramework="net40" />
|
||||||
<package id="NLog" version="2.1.0" targetFramework="net40" />
|
<package id="NLog" version="2.1.0" targetFramework="net40" />
|
||||||
<package id="ValueInjecter" version="2.3.3" targetFramework="net40" />
|
<package id="ValueInjecter" version="2.3.3" targetFramework="net40" />
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Authentication;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
@ -126,9 +127,9 @@ public void GetValue_New_Key()
|
|||||||
[Test]
|
[Test]
|
||||||
public void GetAuthenticationType_No_Existing_Value()
|
public void GetAuthenticationType_No_Existing_Value()
|
||||||
{
|
{
|
||||||
var result = Subject.AuthenticationEnabled;
|
var result = Subject.AuthenticationMethod;
|
||||||
|
|
||||||
result.Should().Be(false);
|
result.Should().Be(AuthenticationType.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
9
src/NzbDrone.Core/Authentication/AuthenticationType.cs
Normal file
9
src/NzbDrone.Core/Authentication/AuthenticationType.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace NzbDrone.Core.Authentication
|
||||||
|
{
|
||||||
|
public enum AuthenticationType
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Basic = 1,
|
||||||
|
Forms = 2
|
||||||
|
}
|
||||||
|
}
|
12
src/NzbDrone.Core/Authentication/User.cs
Normal file
12
src/NzbDrone.Core/Authentication/User.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Authentication
|
||||||
|
{
|
||||||
|
public class User : ModelBase
|
||||||
|
{
|
||||||
|
public Guid Identifier { get; set; }
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
}
|
31
src/NzbDrone.Core/Authentication/UserRepository.cs
Normal file
31
src/NzbDrone.Core/Authentication/UserRepository.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Authentication
|
||||||
|
{
|
||||||
|
public interface IUserRepository : IBasicRepository<User>
|
||||||
|
{
|
||||||
|
User FindUser(string username);
|
||||||
|
User FindUser(Guid identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserRepository : BasicRepository<User>, IUserRepository
|
||||||
|
{
|
||||||
|
public UserRepository(IDatabase database, IEventAggregator eventAggregator)
|
||||||
|
: base(database, eventAggregator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public User FindUser(string username)
|
||||||
|
{
|
||||||
|
return Query.Where(u => u.Username == username).SingleOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public User FindUser(Guid identifier)
|
||||||
|
{
|
||||||
|
return Query.Where(u => u.Identifier == identifier).SingleOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
src/NzbDrone.Core/Authentication/UserService.cs
Normal file
121
src/NzbDrone.Core/Authentication/UserService.cs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Lifecycle;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Authentication
|
||||||
|
{
|
||||||
|
public interface IUserService
|
||||||
|
{
|
||||||
|
User Add(string username, string password);
|
||||||
|
User Update(User user);
|
||||||
|
User Upsert(string username, string password);
|
||||||
|
User FindUser();
|
||||||
|
User FindUser(string username, string password);
|
||||||
|
User FindUser(Guid identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserService : IUserService, IHandle<ApplicationStartedEvent>
|
||||||
|
{
|
||||||
|
private readonly IUserRepository _repo;
|
||||||
|
private readonly IAppFolderInfo _appFolderInfo;
|
||||||
|
private readonly IDiskProvider _diskProvider;
|
||||||
|
|
||||||
|
public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
|
||||||
|
{
|
||||||
|
_repo = repo;
|
||||||
|
_appFolderInfo = appFolderInfo;
|
||||||
|
_diskProvider = diskProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User Add(string username, string password)
|
||||||
|
{
|
||||||
|
return _repo.Insert(new User
|
||||||
|
{
|
||||||
|
Identifier = Guid.NewGuid(),
|
||||||
|
Username = username.ToLowerInvariant(),
|
||||||
|
Password = password.SHA256Hash()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public User Update(User user)
|
||||||
|
{
|
||||||
|
return _repo.Update(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public User Upsert(string username, string password)
|
||||||
|
{
|
||||||
|
var user = FindUser();
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return Add(username, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.Password != password)
|
||||||
|
{
|
||||||
|
user.Password = password.SHA256Hash();
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Username = username.ToLowerInvariant();
|
||||||
|
|
||||||
|
return Update(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public User FindUser()
|
||||||
|
{
|
||||||
|
return _repo.SingleOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public User FindUser(string username, string password)
|
||||||
|
{
|
||||||
|
var user = _repo.FindUser(username.ToLowerInvariant());
|
||||||
|
|
||||||
|
if (user.Password == password.SHA256Hash())
|
||||||
|
{
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User FindUser(Guid identifier)
|
||||||
|
{
|
||||||
|
return _repo.FindUser(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Handle(ApplicationStartedEvent message)
|
||||||
|
{
|
||||||
|
if (_repo.All().Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var configFile = _appFolderInfo.GetConfigPath();
|
||||||
|
|
||||||
|
if (!_diskProvider.FileExists(configFile))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var xDoc = XDocument.Load(configFile);
|
||||||
|
var config = xDoc.Descendants("Config").Single();
|
||||||
|
var usernameElement = config.Descendants("Username").FirstOrDefault();
|
||||||
|
var passwordElement = config.Descendants("Password").FirstOrDefault();
|
||||||
|
|
||||||
|
if (usernameElement == null || passwordElement == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var username = usernameElement.Value;
|
||||||
|
var password = passwordElement.Value;
|
||||||
|
|
||||||
|
Add(username, password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@
|
|||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Authentication;
|
||||||
using NzbDrone.Core.Configuration.Events;
|
using NzbDrone.Core.Configuration.Events;
|
||||||
using NzbDrone.Core.Lifecycle;
|
using NzbDrone.Core.Lifecycle;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
@ -29,14 +30,11 @@ public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>,
|
|||||||
int SslPort { get; }
|
int SslPort { get; }
|
||||||
bool EnableSsl { get; }
|
bool EnableSsl { get; }
|
||||||
bool LaunchBrowser { get; }
|
bool LaunchBrowser { get; }
|
||||||
bool AuthenticationEnabled { get; }
|
AuthenticationType AuthenticationMethod { get; }
|
||||||
bool AnalyticsEnabled { get; }
|
bool AnalyticsEnabled { get; }
|
||||||
string Username { get; }
|
|
||||||
string Password { get; }
|
|
||||||
string LogLevel { get; }
|
string LogLevel { get; }
|
||||||
string Branch { get; }
|
string Branch { get; }
|
||||||
string ApiKey { get; }
|
string ApiKey { get; }
|
||||||
bool Torrent { get; }
|
|
||||||
string SslCertHash { get; }
|
string SslCertHash { get; }
|
||||||
string UrlBase { get; }
|
string UrlBase { get; }
|
||||||
Boolean UpdateAutomatically { get; }
|
Boolean UpdateAutomatically { get; }
|
||||||
@ -163,14 +161,20 @@ public string ApiKey
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Torrent
|
public AuthenticationType AuthenticationMethod
|
||||||
{
|
{
|
||||||
get { return GetValueBoolean("Torrent", false, persist: false); }
|
get
|
||||||
|
{
|
||||||
|
var enabled = GetValueBoolean("AuthenticationEnabled", false, false);
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
SetValue("AuthenticationMethod", AuthenticationType.Basic);
|
||||||
|
return AuthenticationType.Basic;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AuthenticationEnabled
|
return GetValueEnum("AuthenticationMethod", AuthenticationType.None);
|
||||||
{
|
}
|
||||||
get { return GetValueBoolean("AuthenticationEnabled", false); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AnalyticsEnabled
|
public bool AnalyticsEnabled
|
||||||
@ -186,16 +190,6 @@ public string Branch
|
|||||||
get { return GetValue("Branch", "master").ToLowerInvariant(); }
|
get { return GetValue("Branch", "master").ToLowerInvariant(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Username
|
|
||||||
{
|
|
||||||
get { return GetValue("Username", ""); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Password
|
|
||||||
{
|
|
||||||
get { return GetValue("Password", ""); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public string LogLevel
|
public string LogLevel
|
||||||
{
|
{
|
||||||
get { return GetValue("LogLevel", "Info"); }
|
get { return GetValue("LogLevel", "Info"); }
|
||||||
|
@ -289,6 +289,26 @@ public bool CleanupMetadataImages
|
|||||||
set { SetValue("CleanupMetadataImages", value); }
|
set { SetValue("CleanupMetadataImages", value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String RijndaelPassphrase
|
||||||
|
{
|
||||||
|
get { return GetValue("RijndaelPassphrase", Guid.NewGuid().ToString(), true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public String HmacPassphrase
|
||||||
|
{
|
||||||
|
get { return GetValue("HmacPassphrase", Guid.NewGuid().ToString(), true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public String RijndaelSalt
|
||||||
|
{
|
||||||
|
get { return GetValue("RijndaelSalt", Guid.NewGuid().ToString(), true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public String HmacSalt
|
||||||
|
{
|
||||||
|
get { return GetValue("HmacSalt", Guid.NewGuid().ToString(), true); }
|
||||||
|
}
|
||||||
|
|
||||||
private string GetValue(string key)
|
private string GetValue(string key)
|
||||||
{
|
{
|
||||||
return GetValue(key, String.Empty);
|
return GetValue(key, String.Empty);
|
||||||
|
@ -56,9 +56,14 @@ public interface IConfigService
|
|||||||
String TimeFormat { get; set; }
|
String TimeFormat { get; set; }
|
||||||
Boolean ShowRelativeDates { get; set; }
|
Boolean ShowRelativeDates { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Internal
|
//Internal
|
||||||
Boolean CleanupMetadataImages { get; set; }
|
Boolean CleanupMetadataImages { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
//Forms Auth
|
||||||
|
string RijndaelPassphrase { get; }
|
||||||
|
string HmacPassphrase { get; }
|
||||||
|
string RijndaelSalt { get; }
|
||||||
|
string HmacSalt { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
38
src/NzbDrone.Core/Datastore/Converters/GuidConverter.cs
Normal file
38
src/NzbDrone.Core/Datastore/Converters/GuidConverter.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using Marr.Data.Converters;
|
||||||
|
using Marr.Data.Mapping;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Converters
|
||||||
|
{
|
||||||
|
public class GuidConverter : IConverter
|
||||||
|
{
|
||||||
|
public Object FromDB(ConverterContext context)
|
||||||
|
{
|
||||||
|
if (context.DbValue == DBNull.Value)
|
||||||
|
{
|
||||||
|
return Guid.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = (string)context.DbValue;
|
||||||
|
|
||||||
|
return new Guid(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object FromDB(ColumnMap map, Object dbValue)
|
||||||
|
{
|
||||||
|
return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object ToDB(Object clrValue)
|
||||||
|
{
|
||||||
|
var value = clrValue;
|
||||||
|
|
||||||
|
return value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type DbType
|
||||||
|
{
|
||||||
|
get { return typeof(string); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
src/NzbDrone.Core/Datastore/Migration/076_add_users_table.cs
Normal file
17
src/NzbDrone.Core/Datastore/Migration/076_add_users_table.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(76)]
|
||||||
|
public class add_users_table : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Create.TableForModel("Users")
|
||||||
|
.WithColumn("Identifier").AsString().NotNullable().Unique()
|
||||||
|
.WithColumn("Username").AsString().NotNullable().Unique()
|
||||||
|
.WithColumn("Password").AsString().NotNullable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -30,6 +30,7 @@
|
|||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Core.Authentication;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore
|
namespace NzbDrone.Core.Datastore
|
||||||
{
|
{
|
||||||
@ -101,6 +102,7 @@ public static void Map()
|
|||||||
Mapper.Entity<Restriction>().RegisterModel("Restrictions");
|
Mapper.Entity<Restriction>().RegisterModel("Restrictions");
|
||||||
|
|
||||||
Mapper.Entity<DelayProfile>().RegisterModel("DelayProfiles");
|
Mapper.Entity<DelayProfile>().RegisterModel("DelayProfiles");
|
||||||
|
Mapper.Entity<User>().RegisterModel("Users");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterMappers()
|
private static void RegisterMappers()
|
||||||
@ -122,6 +124,7 @@ private static void RegisterMappers()
|
|||||||
MapRepository.Instance.RegisterTypeConverter(typeof(ReleaseInfo), new EmbeddedDocumentConverter());
|
MapRepository.Instance.RegisterTypeConverter(typeof(ReleaseInfo), new EmbeddedDocumentConverter());
|
||||||
MapRepository.Instance.RegisterTypeConverter(typeof(HashSet<Int32>), new EmbeddedDocumentConverter());
|
MapRepository.Instance.RegisterTypeConverter(typeof(HashSet<Int32>), new EmbeddedDocumentConverter());
|
||||||
MapRepository.Instance.RegisterTypeConverter(typeof(OsPath), new OsPathConverter());
|
MapRepository.Instance.RegisterTypeConverter(typeof(OsPath), new OsPathConverter());
|
||||||
|
MapRepository.Instance.RegisterTypeConverter(typeof(Guid), new GuidConverter());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterProviderSettingConverter()
|
private static void RegisterProviderSettingConverter()
|
||||||
|
@ -115,6 +115,10 @@
|
|||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Analytics\AnalyticsService.cs" />
|
<Compile Include="Analytics\AnalyticsService.cs" />
|
||||||
<Compile Include="Annotations\FieldDefinitionAttribute.cs" />
|
<Compile Include="Annotations\FieldDefinitionAttribute.cs" />
|
||||||
|
<Compile Include="Authentication\AuthenticationType.cs" />
|
||||||
|
<Compile Include="Authentication\User.cs" />
|
||||||
|
<Compile Include="Authentication\UserRepository.cs" />
|
||||||
|
<Compile Include="Authentication\UserService.cs" />
|
||||||
<Compile Include="Backup\Backup.cs" />
|
<Compile Include="Backup\Backup.cs" />
|
||||||
<Compile Include="Backup\BackupCommand.cs" />
|
<Compile Include="Backup\BackupCommand.cs" />
|
||||||
<Compile Include="Backup\BackupService.cs" />
|
<Compile Include="Backup\BackupService.cs" />
|
||||||
@ -153,6 +157,7 @@
|
|||||||
<Compile Include="Datastore\Converters\EmbeddedDocumentConverter.cs" />
|
<Compile Include="Datastore\Converters\EmbeddedDocumentConverter.cs" />
|
||||||
<Compile Include="Datastore\Converters\EnumIntConverter.cs" />
|
<Compile Include="Datastore\Converters\EnumIntConverter.cs" />
|
||||||
<Compile Include="Datastore\Converters\Int32Converter.cs" />
|
<Compile Include="Datastore\Converters\Int32Converter.cs" />
|
||||||
|
<Compile Include="Datastore\Converters\GuidConverter.cs" />
|
||||||
<Compile Include="Datastore\Converters\OsPathConverter.cs" />
|
<Compile Include="Datastore\Converters\OsPathConverter.cs" />
|
||||||
<Compile Include="Datastore\Converters\ProviderSettingConverter.cs" />
|
<Compile Include="Datastore\Converters\ProviderSettingConverter.cs" />
|
||||||
<Compile Include="Datastore\Converters\QualityIntConverter.cs" />
|
<Compile Include="Datastore\Converters\QualityIntConverter.cs" />
|
||||||
@ -236,6 +241,7 @@
|
|||||||
<Compile Include="Datastore\Migration\069_quality_proper.cs" />
|
<Compile Include="Datastore\Migration\069_quality_proper.cs" />
|
||||||
<Compile Include="Datastore\Migration\071_unknown_quality_in_profile.cs" />
|
<Compile Include="Datastore\Migration\071_unknown_quality_in_profile.cs" />
|
||||||
<Compile Include="Datastore\Migration\072_history_grabid.cs" />
|
<Compile Include="Datastore\Migration\072_history_grabid.cs" />
|
||||||
|
<Compile Include="Datastore\Migration\076_add_users_table.cs" />
|
||||||
<Compile Include="Datastore\Migration\075_force_lib_update.cs" />
|
<Compile Include="Datastore\Migration\075_force_lib_update.cs" />
|
||||||
<Compile Include="Datastore\Migration\074_disable_eztv.cs" />
|
<Compile Include="Datastore\Migration\074_disable_eztv.cs" />
|
||||||
<Compile Include="Datastore\Migration\073_clear_ratings.cs" />
|
<Compile Include="Datastore\Migration\073_clear_ratings.cs" />
|
||||||
|
@ -284,3 +284,11 @@ dl.info {
|
|||||||
background-color : #17B1D9;
|
background-color : #17B1D9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
color : #ececec;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
vertical-align : bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -35,7 +35,7 @@ define(
|
|||||||
},
|
},
|
||||||
|
|
||||||
onRender: function(){
|
onRender: function(){
|
||||||
if(!this.ui.authToggle.prop('checked')){
|
if(this.ui.authToggle.val() === 'none'){
|
||||||
this.ui.authOptions.hide();
|
this.ui.authOptions.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ define(
|
|||||||
|
|
||||||
_setAuthOptionsVisibility: function () {
|
_setAuthOptionsVisibility: function () {
|
||||||
|
|
||||||
var showAuthOptions = this.ui.authToggle.prop('checked');
|
var showAuthOptions = this.ui.authToggle.val() !== 'none';
|
||||||
|
|
||||||
if (showAuthOptions) {
|
if (showAuthOptions) {
|
||||||
this.ui.authOptions.slideDown();
|
this.ui.authOptions.slideDown();
|
||||||
|
@ -112,21 +112,17 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-3 control-label">Authentication</label>
|
<label class="col-sm-3 control-label">Authentication</label>
|
||||||
|
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-1 col-sm-push-4 help-inline">
|
||||||
<div class="input-group">
|
<i class="icon-nd-form-warning" title="Requires restart to take effect"/>
|
||||||
<label class="checkbox toggle well">
|
|
||||||
<input type="checkbox" class="x-auth" name="authenticationEnabled"/>
|
|
||||||
<p>
|
|
||||||
<span>On</span>
|
|
||||||
<span>Off</span>
|
|
||||||
</p>
|
|
||||||
<div class="btn btn-primary slide-button"/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<span class="help-inline-checkbox">
|
|
||||||
<i class="icon-nd-form-info" title="Require Username and Password to access Sonarr"/>
|
<i class="icon-nd-form-info" title="Require Username and Password to access Sonarr"/>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-4 col-sm-pull-1">
|
||||||
|
<select name="authenticationMethod" class="form-control x-auth">
|
||||||
|
<option value="none">None</option>
|
||||||
|
<option value="basic">Basic (Browser popup)</option>
|
||||||
|
<option value="forms">Forms (Login page)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -296,13 +296,14 @@ define(
|
|||||||
|
|
||||||
app.addInitializer(function () {
|
app.addInitializer(function () {
|
||||||
|
|
||||||
var footerText = serverStatusModel.get('version');
|
var version = serverStatusModel.get('version');
|
||||||
|
var branch = serverStatusModel.get('branch');
|
||||||
|
|
||||||
if (serverStatusModel.get('branch') !== 'master') {
|
$('#footer-region .version').html(version);
|
||||||
footerText += '</br>' + serverStatusModel.get('branch');
|
|
||||||
|
if (branch !== 'master') {
|
||||||
|
$('#footer-region .branch').html(branch);
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#footer-region .version').html(footerText);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
@ -66,6 +66,7 @@
|
|||||||
<div id="footer-region">
|
<div id="footer-region">
|
||||||
Sonarr Ver.
|
Sonarr Ver.
|
||||||
<span class="version"></span>
|
<span class="version"></span>
|
||||||
|
<div class="branch"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
59
src/UI/login.html
Normal file
59
src/UI/login.html
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head runat="server">
|
||||||
|
<title>Sonarr - Login</title>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
|
||||||
|
<link href="/Content/bootstrap.css" rel='stylesheet' type='text/css'/>
|
||||||
|
<link href="/Content/theme.css" rel='stylesheet' type='text/css'/>
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" href="/Content/Images/touch/57.png"/>
|
||||||
|
<link rel="apple-touch-icon" sizes="72x72" href="/Content/Images/touch/72.png"/>
|
||||||
|
<link rel="apple-touch-icon" sizes="114x114" href="/Content/Images/touch/114.png"/>
|
||||||
|
<link rel="apple-touch-icon" sizes="144x144" href="/Content/Images/touch/144.png"/>
|
||||||
|
<link rel="icon" type="image/ico" href="/Content/Images/favicon.ico"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div id="nav-region"></div>
|
||||||
|
</div>
|
||||||
|
<div id="page">
|
||||||
|
<div class="page-container">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="container-fluid main-region" id="main-region">
|
||||||
|
<div class="col-md-2 col-md-offset-5">
|
||||||
|
<form name="login" id="login" class="login" method="POST">
|
||||||
|
<h2><img src="/Content/Images/logos/32.png" alt=""/> Sonarr</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username" class="sr-only">Email address</label>
|
||||||
|
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password" class="sr-only">Password</label>
|
||||||
|
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="rememberMe" checked="checked"> Remember me
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input class="btn btn-lg btn-primary btn-block" type="submit" value="Log in" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="modal-region"></div>
|
||||||
|
<div id="file-browser-modal-region"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a id="scroll-up" title="Back to the top!">
|
||||||
|
<i class="icon-circle-arrow-up"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user