mirror of
https://github.com/Radarr/Radarr.git
synced 2024-09-17 15:02:34 +02:00
New: Forms authentication
This commit is contained in:
parent
7c38fcb9f3
commit
3c756348eb
@ -9,5 +9,5 @@ require('./copy');
|
||||
|
||||
gulp.task('build', function () {
|
||||
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));
|
||||
});
|
||||
|
||||
gulp.task('copyIndex', function () {
|
||||
return gulp.src(paths.src.index)
|
||||
.pipe(cache('copyIndex'))
|
||||
gulp.task('copyHtml', function () {
|
||||
return gulp.src(paths.src.html)
|
||||
.pipe(cache('copyHtml'))
|
||||
.pipe(gulp.dest(paths.dest.root));
|
||||
});
|
||||
|
||||
|
@ -2,7 +2,7 @@ module.exports = {
|
||||
src: {
|
||||
root: './src/UI/',
|
||||
templates: './src/UI/**/*.hbs',
|
||||
index: './src/UI/index.html',
|
||||
html: './src/UI/*.html',
|
||||
partials: './src/UI/**/*Partial.hbs',
|
||||
scripts: './src/UI/**/*.js',
|
||||
less: ['./src/UI/**/*.less'],
|
||||
|
@ -10,11 +10,11 @@ require('./less.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.templates, ['handlebars']);
|
||||
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']);
|
||||
});
|
||||
|
||||
@ -23,7 +23,8 @@ gulp.task('liveReload', ['jshint', 'handlebars', 'less', 'copyJs'], function ()
|
||||
gulp.watch([
|
||||
'app/**/*.js',
|
||||
'app/**/*.css',
|
||||
'app/index.html'
|
||||
'app/index.html',
|
||||
'app/login.html'
|
||||
]).on('change', function (file) {
|
||||
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 Nancy;
|
||||
using Nancy.Authentication.Basic;
|
||||
using Nancy.Authentication.Forms;
|
||||
using Nancy.Security;
|
||||
using NzbDrone.Api.Extensions;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
||||
namespace NzbDrone.Api.Authentication
|
||||
{
|
||||
public interface IAuthenticationService : IUserValidator
|
||||
public interface IAuthenticationService : IUserValidator, IUserMapper
|
||||
{
|
||||
bool IsAuthenticated(NancyContext context);
|
||||
}
|
||||
@ -17,37 +19,49 @@ public interface IAuthenticationService : IUserValidator
|
||||
public class AuthenticationService : IAuthenticationService
|
||||
{
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IUserService _userService;
|
||||
private static readonly NzbDroneUser AnonymousUser = new NzbDroneUser { UserName = "Anonymous" };
|
||||
private static String API_KEY;
|
||||
|
||||
public AuthenticationService(IConfigFileProvider configFileProvider)
|
||||
public AuthenticationService(IConfigFileProvider configFileProvider, IUserService userService)
|
||||
{
|
||||
_configFileProvider = configFileProvider;
|
||||
_userService = userService;
|
||||
API_KEY = configFileProvider.ApiKey;
|
||||
}
|
||||
|
||||
public IUserIdentity Validate(string username, string password)
|
||||
{
|
||||
if (!Enabled)
|
||||
if (_configFileProvider.AuthenticationMethod == AuthenticationType.None)
|
||||
{
|
||||
return AnonymousUser;
|
||||
}
|
||||
|
||||
if (_configFileProvider.Username.Equals(username) &&
|
||||
_configFileProvider.Password.Equals(password))
|
||||
var user = _userService.FindUser(username, password);
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
return new NzbDroneUser { UserName = username };
|
||||
return new NzbDroneUser { UserName = user.Username };
|
||||
}
|
||||
|
||||
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)
|
||||
@ -59,13 +73,13 @@ public bool IsAuthenticated(NancyContext context)
|
||||
return ValidApiKey(apiKey);
|
||||
}
|
||||
|
||||
if (_configFileProvider.AuthenticationMethod == AuthenticationType.None)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (context.Request.IsFeedRequest())
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ValidUser(context) || ValidApiKey(apiKey))
|
||||
{
|
||||
return true;
|
||||
@ -74,7 +88,12 @@ public bool IsAuthenticated(NancyContext context)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Enabled)
|
||||
if (context.Request.IsLoginRequest())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (context.Request.IsContentRequest())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
@ -1,23 +1,46 @@
|
||||
using Nancy;
|
||||
using System;
|
||||
using System.Text;
|
||||
using Nancy;
|
||||
using Nancy.Authentication.Basic;
|
||||
using Nancy.Authentication.Forms;
|
||||
using Nancy.Bootstrapper;
|
||||
using Nancy.Cryptography;
|
||||
using NzbDrone.Api.Extensions;
|
||||
using NzbDrone.Api.Extensions.Pipelines;
|
||||
using NzbDrone.Core.Authentication;
|
||||
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)
|
||||
public EnableAuthInNancy(IAuthenticationService authenticationService,
|
||||
IConfigService configService,
|
||||
IConfigFileProvider configFileProvider)
|
||||
{
|
||||
_authenticationService = authenticationService;
|
||||
_configService = configService;
|
||||
_configFileProvider = configFileProvider;
|
||||
}
|
||||
|
||||
public void Register(IPipelines pipelines)
|
||||
{
|
||||
pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Sonarr"));
|
||||
if (_configFileProvider.AuthenticationMethod == AuthenticationType.Forms)
|
||||
{
|
||||
RegisterFormsAuth(pipelines);
|
||||
}
|
||||
|
||||
else if (_configFileProvider.AuthenticationMethod == AuthenticationType.Basic)
|
||||
{
|
||||
pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Sonarr"));
|
||||
}
|
||||
|
||||
pipelines.BeforeRequest.AddItemToEndOfPipeline(RequiresAuthentication);
|
||||
pipelines.AfterRequest.AddItemToEndOfPipeline(RemoveLoginHooksForApiCalls);
|
||||
}
|
||||
|
||||
private Response RequiresAuthentication(NancyContext context)
|
||||
@ -31,5 +54,33 @@ private Response RequiresAuthentication(NancyContext context)
|
||||
|
||||
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 FluentValidation;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Update;
|
||||
using NzbDrone.Core.Validation;
|
||||
@ -13,11 +15,13 @@ namespace NzbDrone.Api.Config
|
||||
public class HostConfigModule : NzbDroneRestModule<HostConfigResource>
|
||||
{
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public HostConfigModule(IConfigFileProvider configFileProvider)
|
||||
public HostConfigModule(IConfigFileProvider configFileProvider, IUserService userService)
|
||||
: base("/config/host")
|
||||
{
|
||||
_configFileProvider = configFileProvider;
|
||||
_userService = userService;
|
||||
|
||||
GetResourceSingle = 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.Port).ValidPort();
|
||||
|
||||
SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationEnabled);
|
||||
SharedValidator.RuleFor(c => c.Password).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.AuthenticationMethod != AuthenticationType.None);
|
||||
|
||||
SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl);
|
||||
SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl && OsInfo.IsWindows);
|
||||
@ -46,6 +50,14 @@ private HostConfigResource GetHostConfig()
|
||||
resource.InjectFrom(_configFileProvider);
|
||||
resource.Id = 1;
|
||||
|
||||
var user = _userService.FindUser();
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
resource.Username = user.Username;
|
||||
resource.Password = user.Password;
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
@ -61,6 +73,11 @@ private void SaveHostConfig(HostConfigResource resource)
|
||||
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
|
||||
|
||||
_configFileProvider.SaveConfigDictionary(dictionary);
|
||||
|
||||
if (resource.Username.IsNotNullOrWhiteSpace() && resource.Password.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
_userService.Upsert(resource.Username, resource.Password);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using NzbDrone.Api.REST;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Update;
|
||||
|
||||
namespace NzbDrone.Api.Config
|
||||
@ -11,7 +12,7 @@ public class HostConfigResource : RestResource
|
||||
public Int32 SslPort { get; set; }
|
||||
public Boolean EnableSsl { get; set; }
|
||||
public Boolean LaunchBrowser { get; set; }
|
||||
public bool AuthenticationEnabled { get; set; }
|
||||
public AuthenticationType AuthenticationMethod { get; set; }
|
||||
public Boolean AnalyticsEnabled { get; set; }
|
||||
public String Username { 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("::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)
|
||||
{
|
||||
return !resourceUrl.Contains(".");
|
||||
return !resourceUrl.Contains(".") && !resourceUrl.StartsWith("/login");
|
||||
}
|
||||
|
||||
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>
|
||||
<HintPath>..\packages\Nancy.Authentication.Basic.0.23.2\lib\net40\Nancy.Authentication.Basic.dll</HintPath>
|
||||
</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">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\Newtonsoft.Json.6.0.6\lib\net40\Newtonsoft.Json.dll</HintPath>
|
||||
@ -80,6 +83,8 @@
|
||||
</Compile>
|
||||
<Compile Include="Authentication\AuthenticationService.cs" />
|
||||
<Compile Include="Authentication\EnableAuthInNancy.cs" />
|
||||
<Compile Include="Authentication\LoginModule.cs" />
|
||||
<Compile Include="Authentication\LoginResource.cs" />
|
||||
<Compile Include="Authentication\NzbDroneUser.cs" />
|
||||
<Compile Include="Blacklist\BlacklistModule.cs" />
|
||||
<Compile Include="Blacklist\BlacklistResource.cs" />
|
||||
@ -94,6 +99,7 @@
|
||||
<Compile Include="Commands\CommandResource.cs" />
|
||||
<Compile Include="Extensions\AccessControlHeaders.cs" />
|
||||
<Compile Include="Extensions\Pipelines\CorsPipeline.cs" />
|
||||
<Compile Include="Frontend\Mappers\LoginHtmlMapper.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileModule.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileResource.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileValidator.cs" />
|
||||
|
@ -57,7 +57,7 @@ private Response GetStatus()
|
||||
IsOsx = OsInfo.IsOsx,
|
||||
IsWindows = OsInfo.IsWindows,
|
||||
Branch = _configFileProvider.Branch,
|
||||
Authentication = _configFileProvider.AuthenticationEnabled,
|
||||
Authentication = _configFileProvider.AuthenticationMethod,
|
||||
SqliteVersion = _database.Version,
|
||||
UrlBase = _configFileProvider.UrlBase,
|
||||
RuntimeVersion = _runtimeInfo.RuntimeVersion
|
||||
|
@ -4,6 +4,7 @@
|
||||
<package id="FluentValidation" version="5.5.0.0" 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.Forms" version="0.23.2" targetFramework="net40" />
|
||||
<package id="Newtonsoft.Json" version="6.0.6" targetFramework="net40" />
|
||||
<package id="NLog" version="2.1.0" targetFramework="net40" />
|
||||
<package id="ValueInjecter" version="2.3.3" targetFramework="net40" />
|
||||
|
@ -3,6 +3,7 @@
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
@ -126,9 +127,9 @@ public void GetValue_New_Key()
|
||||
[Test]
|
||||
public void GetAuthenticationType_No_Existing_Value()
|
||||
{
|
||||
var result = Subject.AuthenticationEnabled;
|
||||
var result = Subject.AuthenticationMethod;
|
||||
|
||||
result.Should().Be(false);
|
||||
result.Should().Be(AuthenticationType.None);
|
||||
}
|
||||
|
||||
[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.EnvironmentInfo;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration.Events;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
@ -29,14 +30,11 @@ public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>,
|
||||
int SslPort { get; }
|
||||
bool EnableSsl { get; }
|
||||
bool LaunchBrowser { get; }
|
||||
bool AuthenticationEnabled { get; }
|
||||
AuthenticationType AuthenticationMethod { get; }
|
||||
bool AnalyticsEnabled { get; }
|
||||
string Username { get; }
|
||||
string Password { get; }
|
||||
string LogLevel { get; }
|
||||
string Branch { get; }
|
||||
string ApiKey { get; }
|
||||
bool Torrent { get; }
|
||||
string SslCertHash { get; }
|
||||
string UrlBase { 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);
|
||||
|
||||
public bool AuthenticationEnabled
|
||||
{
|
||||
get { return GetValueBoolean("AuthenticationEnabled", false); }
|
||||
if (enabled)
|
||||
{
|
||||
SetValue("AuthenticationMethod", AuthenticationType.Basic);
|
||||
return AuthenticationType.Basic;
|
||||
}
|
||||
|
||||
return GetValueEnum("AuthenticationMethod", AuthenticationType.None);
|
||||
}
|
||||
}
|
||||
|
||||
public bool AnalyticsEnabled
|
||||
@ -186,16 +190,6 @@ public string Branch
|
||||
get { return GetValue("Branch", "master").ToLowerInvariant(); }
|
||||
}
|
||||
|
||||
public string Username
|
||||
{
|
||||
get { return GetValue("Username", ""); }
|
||||
}
|
||||
|
||||
public string Password
|
||||
{
|
||||
get { return GetValue("Password", ""); }
|
||||
}
|
||||
|
||||
public string LogLevel
|
||||
{
|
||||
get { return GetValue("LogLevel", "Info"); }
|
||||
|
@ -289,6 +289,26 @@ public bool CleanupMetadataImages
|
||||
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)
|
||||
{
|
||||
return GetValue(key, String.Empty);
|
||||
|
@ -56,9 +56,14 @@ public interface IConfigService
|
||||
String TimeFormat { get; set; }
|
||||
Boolean ShowRelativeDates { get; set; }
|
||||
|
||||
|
||||
|
||||
//Internal
|
||||
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.Tv;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Authentication;
|
||||
|
||||
namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
@ -101,6 +102,7 @@ public static void Map()
|
||||
Mapper.Entity<Restriction>().RegisterModel("Restrictions");
|
||||
|
||||
Mapper.Entity<DelayProfile>().RegisterModel("DelayProfiles");
|
||||
Mapper.Entity<User>().RegisterModel("Users");
|
||||
}
|
||||
|
||||
private static void RegisterMappers()
|
||||
@ -122,6 +124,7 @@ private static void RegisterMappers()
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(ReleaseInfo), new EmbeddedDocumentConverter());
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(HashSet<Int32>), new EmbeddedDocumentConverter());
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(OsPath), new OsPathConverter());
|
||||
MapRepository.Instance.RegisterTypeConverter(typeof(Guid), new GuidConverter());
|
||||
}
|
||||
|
||||
private static void RegisterProviderSettingConverter()
|
||||
|
@ -115,6 +115,10 @@
|
||||
</Compile>
|
||||
<Compile Include="Analytics\AnalyticsService.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\BackupCommand.cs" />
|
||||
<Compile Include="Backup\BackupService.cs" />
|
||||
@ -153,6 +157,7 @@
|
||||
<Compile Include="Datastore\Converters\EmbeddedDocumentConverter.cs" />
|
||||
<Compile Include="Datastore\Converters\EnumIntConverter.cs" />
|
||||
<Compile Include="Datastore\Converters\Int32Converter.cs" />
|
||||
<Compile Include="Datastore\Converters\GuidConverter.cs" />
|
||||
<Compile Include="Datastore\Converters\OsPathConverter.cs" />
|
||||
<Compile Include="Datastore\Converters\ProviderSettingConverter.cs" />
|
||||
<Compile Include="Datastore\Converters\QualityIntConverter.cs" />
|
||||
@ -236,6 +241,7 @@
|
||||
<Compile Include="Datastore\Migration\069_quality_proper.cs" />
|
||||
<Compile Include="Datastore\Migration\071_unknown_quality_in_profile.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\074_disable_eztv.cs" />
|
||||
<Compile Include="Datastore\Migration\073_clear_ratings.cs" />
|
||||
|
@ -284,3 +284,11 @@ dl.info {
|
||||
background-color : #17B1D9;
|
||||
}
|
||||
}
|
||||
|
||||
.login {
|
||||
color : #ececec;
|
||||
|
||||
h2 {
|
||||
vertical-align : bottom;
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ define(
|
||||
},
|
||||
|
||||
onRender: function(){
|
||||
if(!this.ui.authToggle.prop('checked')){
|
||||
if(this.ui.authToggle.val() === 'none'){
|
||||
this.ui.authOptions.hide();
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@ define(
|
||||
|
||||
_setAuthOptionsVisibility: function () {
|
||||
|
||||
var showAuthOptions = this.ui.authToggle.prop('checked');
|
||||
var showAuthOptions = this.ui.authToggle.val() !== 'none';
|
||||
|
||||
if (showAuthOptions) {
|
||||
this.ui.authOptions.slideDown();
|
||||
|
@ -112,21 +112,17 @@
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Authentication</label>
|
||||
|
||||
<div class="col-sm-8">
|
||||
<div class="input-group">
|
||||
<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>
|
||||
<div class="col-sm-1 col-sm-push-4 help-inline">
|
||||
<i class="icon-nd-form-warning" title="Requires restart to take effect"/>
|
||||
<i class="icon-nd-form-info" title="Require Username and Password to access Sonarr"/>
|
||||
</div>
|
||||
|
||||
<span class="help-inline-checkbox">
|
||||
<i class="icon-nd-form-info" title="Require Username and Password to access Sonarr"/>
|
||||
</span>
|
||||
</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>
|
||||
|
||||
|
@ -296,13 +296,14 @@ define(
|
||||
|
||||
app.addInitializer(function () {
|
||||
|
||||
var footerText = serverStatusModel.get('version');
|
||||
var version = serverStatusModel.get('version');
|
||||
var branch = serverStatusModel.get('branch');
|
||||
|
||||
if (serverStatusModel.get('branch') !== 'master') {
|
||||
footerText += '</br>' + serverStatusModel.get('branch');
|
||||
$('#footer-region .version').html(version);
|
||||
|
||||
if (branch !== 'master') {
|
||||
$('#footer-region .branch').html(branch);
|
||||
}
|
||||
|
||||
$('#footer-region .version').html(footerText);
|
||||
});
|
||||
|
||||
return app;
|
||||
|
@ -66,6 +66,7 @@
|
||||
<div id="footer-region">
|
||||
Sonarr Ver.
|
||||
<span class="version"></span>
|
||||
<div class="branch"></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