mirror of
https://github.com/Sonarr/Sonarr.git
synced 2024-12-16 11:37:58 +02:00
API Key in UI
New: view/reset API in General Settings Fixed: API will reject unauthenticated requests
This commit is contained in:
parent
0914441de7
commit
6b423c104c
@ -78,6 +78,7 @@ module.exports = function (grunt) {
|
|||||||
'**/*.png',
|
'**/*.png',
|
||||||
'**/*.jpg',
|
'**/*.jpg',
|
||||||
'**/*.ico',
|
'**/*.ico',
|
||||||
|
'**/*.swf',
|
||||||
'**/FontAwesome/*.*',
|
'**/FontAwesome/*.*',
|
||||||
'**/fonts/*.*'
|
'**/fonts/*.*'
|
||||||
],
|
],
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using Nancy.Bootstrapper;
|
using Nancy.Bootstrapper;
|
||||||
using NzbDrone.Api.Extensions;
|
using NzbDrone.Api.Extensions;
|
||||||
using NzbDrone.Api.Extensions.Pipelines;
|
using NzbDrone.Api.Extensions.Pipelines;
|
||||||
|
using NzbDrone.Common;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
@ -12,12 +13,12 @@ namespace NzbDrone.Api.Authentication
|
|||||||
public class EnableStatelessAuthInNancy : IRegisterNancyPipeline
|
public class EnableStatelessAuthInNancy : IRegisterNancyPipeline
|
||||||
{
|
{
|
||||||
private readonly IAuthenticationService _authenticationService;
|
private readonly IAuthenticationService _authenticationService;
|
||||||
private readonly IConfigFileProvider _configFileProvider;
|
private static String API_KEY;
|
||||||
|
|
||||||
public EnableStatelessAuthInNancy(IAuthenticationService authenticationService, IConfigFileProvider configFileProvider)
|
public EnableStatelessAuthInNancy(IAuthenticationService authenticationService, IConfigFileProvider configFileProvider)
|
||||||
{
|
{
|
||||||
_authenticationService = authenticationService;
|
_authenticationService = authenticationService;
|
||||||
_configFileProvider = configFileProvider;
|
API_KEY = configFileProvider.ApiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Register(IPipelines pipelines)
|
public void Register(IPipelines pipelines)
|
||||||
@ -36,9 +37,9 @@ public Response ValidateApiKey(NancyContext context)
|
|||||||
|
|
||||||
var authorizationHeader = context.Request.Headers.Authorization;
|
var authorizationHeader = context.Request.Headers.Authorization;
|
||||||
var apiKeyHeader = context.Request.Headers["X-Api-Key"].FirstOrDefault();
|
var apiKeyHeader = context.Request.Headers["X-Api-Key"].FirstOrDefault();
|
||||||
var apiKey = String.IsNullOrWhiteSpace(apiKeyHeader) ? authorizationHeader : apiKeyHeader;
|
var apiKey = apiKeyHeader.IsNullOrWhiteSpace() ? authorizationHeader : apiKeyHeader;
|
||||||
|
|
||||||
if (context.Request.IsApiRequest() && !ValidApiKey(apiKey) && !_authenticationService.IsAuthenticated(context))
|
if (context.Request.IsApiRequest() && !ValidApiKey(apiKey) && !IsAuthenticated(context))
|
||||||
{
|
{
|
||||||
response = new Response { StatusCode = HttpStatusCode.Unauthorized };
|
response = new Response { StatusCode = HttpStatusCode.Unauthorized };
|
||||||
}
|
}
|
||||||
@ -48,10 +49,15 @@ public Response ValidateApiKey(NancyContext context)
|
|||||||
|
|
||||||
private bool ValidApiKey(string apiKey)
|
private bool ValidApiKey(string apiKey)
|
||||||
{
|
{
|
||||||
if (String.IsNullOrWhiteSpace(apiKey)) return false;
|
if (apiKey.IsNullOrWhiteSpace()) return false;
|
||||||
if (!apiKey.Equals(_configFileProvider.ApiKey)) return false;
|
if (!apiKey.Equals(API_KEY)) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsAuthenticated(NancyContext context)
|
||||||
|
{
|
||||||
|
return _authenticationService.Enabled && _authenticationService.IsAuthenticated(context);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +1,8 @@
|
|||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Nancy;
|
using Nancy;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common;
|
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
@ -12,10 +12,12 @@ namespace NzbDrone.Api.Frontend.Mappers
|
|||||||
public class IndexHtmlMapper : StaticResourceMapperBase
|
public class IndexHtmlMapper : StaticResourceMapperBase
|
||||||
{
|
{
|
||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
private readonly IConfigFileProvider _configFileProvider;
|
|
||||||
private readonly string _indexPath;
|
private readonly string _indexPath;
|
||||||
private static readonly Regex ReplaceRegex = new Regex("(?<=(?:href|src|data-main)=\").*?(?=\")", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex ReplaceRegex = new Regex("(?<=(?:href|src|data-main)=\").*?(?=\")", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
private static String API_KEY;
|
||||||
|
private static String URL_BASE;
|
||||||
|
|
||||||
public IndexHtmlMapper(IAppFolderInfo appFolderInfo,
|
public IndexHtmlMapper(IAppFolderInfo appFolderInfo,
|
||||||
IDiskProvider diskProvider,
|
IDiskProvider diskProvider,
|
||||||
IConfigFileProvider configFileProvider,
|
IConfigFileProvider configFileProvider,
|
||||||
@ -23,8 +25,10 @@ public IndexHtmlMapper(IAppFolderInfo appFolderInfo,
|
|||||||
: base(diskProvider, logger)
|
: base(diskProvider, logger)
|
||||||
{
|
{
|
||||||
_diskProvider = diskProvider;
|
_diskProvider = diskProvider;
|
||||||
_configFileProvider = configFileProvider;
|
|
||||||
_indexPath = Path.Combine(appFolderInfo.StartUpFolder, "UI", "index.html");
|
_indexPath = Path.Combine(appFolderInfo.StartUpFolder, "UI", "index.html");
|
||||||
|
|
||||||
|
API_KEY = configFileProvider.ApiKey;
|
||||||
|
URL_BASE = configFileProvider.UrlBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string Map(string resourceUrl)
|
protected override string Map(string resourceUrl)
|
||||||
@ -54,12 +58,12 @@ private string GetIndexText()
|
|||||||
{
|
{
|
||||||
var text = _diskProvider.ReadAllText(_indexPath);
|
var text = _diskProvider.ReadAllText(_indexPath);
|
||||||
|
|
||||||
text = ReplaceRegex.Replace(text, match => _configFileProvider.UrlBase + match.Value);
|
text = ReplaceRegex.Replace(text, match => URL_BASE + match.Value);
|
||||||
|
|
||||||
text = text.Replace(".css", ".css?v=" + BuildInfo.Version);
|
text = text.Replace(".css", ".css?v=" + BuildInfo.Version);
|
||||||
text = text.Replace(".js", ".js?v=" + BuildInfo.Version);
|
text = text.Replace(".js", ".js?v=" + BuildInfo.Version);
|
||||||
text = text.Replace("API_ROOT", _configFileProvider.UrlBase + "/api");
|
text = text.Replace("API_ROOT", URL_BASE + "/api");
|
||||||
text = text.Replace("API_KEY", _configFileProvider.ApiKey);
|
text = text.Replace("API_KEY", API_KEY);
|
||||||
text = text.Replace("APP_VERSION", BuildInfo.Version.ToString());
|
text = text.Replace("APP_VERSION", BuildInfo.Version.ToString());
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
|
@ -26,7 +26,11 @@ protected override string Map(string resourceUrl)
|
|||||||
|
|
||||||
public override bool CanHandle(string resourceUrl)
|
public override bool CanHandle(string resourceUrl)
|
||||||
{
|
{
|
||||||
return resourceUrl.StartsWith("/Content") || resourceUrl.EndsWith(".js") || resourceUrl.EndsWith(".css") || resourceUrl.EndsWith(".ico");
|
return resourceUrl.StartsWith("/Content") ||
|
||||||
|
resourceUrl.EndsWith(".js") ||
|
||||||
|
resourceUrl.EndsWith(".css") ||
|
||||||
|
resourceUrl.EndsWith(".ico") ||
|
||||||
|
resourceUrl.EndsWith(".swf");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,12 +9,14 @@
|
|||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
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.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Configuration
|
namespace NzbDrone.Core.Configuration
|
||||||
{
|
{
|
||||||
public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>
|
public interface IConfigFileProvider : IHandleAsync<ApplicationStartedEvent>,
|
||||||
|
IExecute<ResetApiKeyCommand>
|
||||||
{
|
{
|
||||||
Dictionary<string, object> GetConfigDictionary();
|
Dictionary<string, object> GetConfigDictionary();
|
||||||
void SaveConfigDictionary(Dictionary<string, object> configValues);
|
void SaveConfigDictionary(Dictionary<string, object> configValues);
|
||||||
@ -76,6 +78,11 @@ public void SaveConfigDictionary(Dictionary<string, object> configValues)
|
|||||||
|
|
||||||
foreach (var configValue in configValues)
|
foreach (var configValue in configValues)
|
||||||
{
|
{
|
||||||
|
if (configValue.Key.Equals("ApiKey", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
object currentValue;
|
object currentValue;
|
||||||
allWithDefaults.TryGetValue(configValue.Key, out currentValue);
|
allWithDefaults.TryGetValue(configValue.Key, out currentValue);
|
||||||
if (currentValue == null) continue;
|
if (currentValue == null) continue;
|
||||||
@ -115,7 +122,7 @@ public string ApiKey
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return GetValue("ApiKey", Guid.NewGuid().ToString().Replace("-", ""));
|
return GetValue("ApiKey", GenerateApiKey());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,9 +303,19 @@ private XDocument LoadConfigFile()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GenerateApiKey()
|
||||||
|
{
|
||||||
|
return Guid.NewGuid().ToString().Replace("-", "");
|
||||||
|
}
|
||||||
|
|
||||||
public void HandleAsync(ApplicationStartedEvent message)
|
public void HandleAsync(ApplicationStartedEvent message)
|
||||||
{
|
{
|
||||||
DeleteOldValues();
|
DeleteOldValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Execute(ResetApiKeyCommand message)
|
||||||
|
{
|
||||||
|
SetValue("ApiKey", GenerateApiKey());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
src/NzbDrone.Core/Configuration/ResetApiKeyCommand.cs
Normal file
15
src/NzbDrone.Core/Configuration/ResetApiKeyCommand.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Configuration
|
||||||
|
{
|
||||||
|
public class ResetApiKeyCommand : Command
|
||||||
|
{
|
||||||
|
public override bool SendUpdatesToClient
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Core.Configuration.Events;
|
using NzbDrone.Core.Configuration.Events;
|
||||||
|
@ -114,6 +114,7 @@
|
|||||||
<Compile Include="Configuration\Events\ConfigSavedEvent.cs" />
|
<Compile Include="Configuration\Events\ConfigSavedEvent.cs" />
|
||||||
<Compile Include="Configuration\IConfigService.cs" />
|
<Compile Include="Configuration\IConfigService.cs" />
|
||||||
<Compile Include="Configuration\InvalidConfigFileException.cs" />
|
<Compile Include="Configuration\InvalidConfigFileException.cs" />
|
||||||
|
<Compile Include="Configuration\ResetApiKeyCommand.cs" />
|
||||||
<Compile Include="DataAugmentation\DailySeries\DailySeriesDataProxy.cs" />
|
<Compile Include="DataAugmentation\DailySeries\DailySeriesDataProxy.cs" />
|
||||||
<Compile Include="DataAugmentation\DailySeries\DailySeriesService.cs" />
|
<Compile Include="DataAugmentation\DailySeries\DailySeriesService.cs" />
|
||||||
<Compile Include="DataAugmentation\Scene\SceneMapping.cs" />
|
<Compile Include="DataAugmentation\Scene\SceneMapping.cs" />
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
color : #595959;
|
color : #595959;
|
||||||
margin-right : 5px;
|
margin-right : 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
width : 100px;
|
width : 100px;
|
||||||
margin-left : 0px;
|
margin-left : 0px;
|
||||||
@ -24,7 +25,8 @@
|
|||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
i {
|
i {
|
||||||
margin-right: 0px;
|
margin-right : 0px;
|
||||||
|
color : inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
src/UI/Content/zero.clipboard.swf
Normal file
BIN
src/UI/Content/zero.clipboard.swf
Normal file
Binary file not shown.
1010
src/UI/JsLibraries/zero.clipboard.js
Normal file
1010
src/UI/JsLibraries/zero.clipboard.js
Normal file
File diff suppressed because it is too large
Load Diff
30
src/UI/Mixins/CopyToClipboard.js
Normal file
30
src/UI/Mixins/CopyToClipboard.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
define([
|
||||||
|
'jquery',
|
||||||
|
'System/StatusModel',
|
||||||
|
'zero.clipboard',
|
||||||
|
'Shared/Messenger'
|
||||||
|
],
|
||||||
|
function ($, StatusModel, ZeroClipboard, Messenger) {
|
||||||
|
|
||||||
|
$.fn.copyToClipboard = function (input) {
|
||||||
|
var moviePath = StatusModel.get('urlBase') + '/Content/zero.clipboard.swf';
|
||||||
|
|
||||||
|
var client = new ZeroClipboard(this, {
|
||||||
|
moviePath: moviePath
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('load', function(client) {
|
||||||
|
client.on('dataRequested', function (client) {
|
||||||
|
client.setText(input.val());
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('complete', function() {
|
||||||
|
Messenger.show({
|
||||||
|
message: 'Copied text to clipboard'
|
||||||
|
});
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
});
|
@ -1,23 +1,34 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
define(
|
define(
|
||||||
[
|
[
|
||||||
|
'vent',
|
||||||
'marionette',
|
'marionette',
|
||||||
|
'Commands/CommandController',
|
||||||
'Mixins/AsModelBoundView',
|
'Mixins/AsModelBoundView',
|
||||||
'Mixins/AsValidatedView'
|
'Mixins/AsValidatedView',
|
||||||
], function (Marionette, AsModelBoundView, AsValidatedView) {
|
'Mixins/CopyToClipboard'
|
||||||
|
], function (vent, Marionette, CommandController, AsModelBoundView, AsValidatedView) {
|
||||||
var view = Marionette.ItemView.extend({
|
var view = Marionette.ItemView.extend({
|
||||||
template: 'Settings/General/GeneralViewTemplate',
|
template: 'Settings/General/GeneralViewTemplate',
|
||||||
|
|
||||||
events: {
|
events: {
|
||||||
'change .x-auth': '_setAuthOptionsVisibility',
|
'change .x-auth' : '_setAuthOptionsVisibility',
|
||||||
'change .x-ssl': '_setSslOptionsVisibility'
|
'change .x-ssl' : '_setSslOptionsVisibility',
|
||||||
|
'click .x-reset-api-key' : '_resetApiKey'
|
||||||
},
|
},
|
||||||
|
|
||||||
ui: {
|
ui: {
|
||||||
authToggle : '.x-auth',
|
authToggle : '.x-auth',
|
||||||
authOptions: '.x-auth-options',
|
authOptions : '.x-auth-options',
|
||||||
sslToggle : '.x-ssl',
|
sslToggle : '.x-ssl',
|
||||||
sslOptions: '.x-ssl-options'
|
sslOptions : '.x-ssl-options',
|
||||||
|
resetApiKey : '.x-reset-api-key',
|
||||||
|
copyApiKey : '.x-copy-api-key',
|
||||||
|
apiKeyInput : '.x-api-key'
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function () {
|
||||||
|
vent.on(vent.Events.CommandComplete, this._commandComplete, this);
|
||||||
},
|
},
|
||||||
|
|
||||||
onRender: function(){
|
onRender: function(){
|
||||||
@ -28,6 +39,17 @@ define(
|
|||||||
if(!this.ui.sslToggle.prop('checked')){
|
if(!this.ui.sslToggle.prop('checked')){
|
||||||
this.ui.sslOptions.hide();
|
this.ui.sslOptions.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CommandController.bindToCommand({
|
||||||
|
element: this.ui.resetApiKey,
|
||||||
|
command: {
|
||||||
|
name: 'resetApiKey'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onShow: function () {
|
||||||
|
this.ui.copyApiKey.copyToClipboard(this.ui.apiKeyInput);
|
||||||
},
|
},
|
||||||
|
|
||||||
_setAuthOptionsVisibility: function () {
|
_setAuthOptionsVisibility: function () {
|
||||||
@ -54,6 +76,20 @@ define(
|
|||||||
else {
|
else {
|
||||||
this.ui.sslOptions.slideUp();
|
this.ui.sslOptions.slideUp();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_resetApiKey: function () {
|
||||||
|
if (window.confirm("Reset API Key?")) {
|
||||||
|
CommandController.Execute('resetApiKey', {
|
||||||
|
name : 'resetApiKey'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_commandComplete: function (options) {
|
||||||
|
if (options.command.get('name') === 'resetapikey') {
|
||||||
|
this.model.fetch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -119,6 +119,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group api-key">
|
||||||
|
<label class="control-label">API Key</label>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="input-append">
|
||||||
|
<input type="text" name="apiKey" readonly="readonly" class="x-api-key"/>
|
||||||
|
<button class="btn btn-icon-only x-copy-api-key" title="Copy to clipboard"><i class="icon-copy"></i></button>
|
||||||
|
<button class="btn btn-danger btn-icon-only x-reset-api-key" title="Reset API Key"><i class="icon-refresh"></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<i class="icon-nd-form-warning" title="Requires restart to take effect"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
@ -155,27 +170,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if_mono}}
|
<!--{{#if_mono}}-->
|
||||||
<div class="control-group">
|
<!--<div class="control-group">-->
|
||||||
<label class="control-label">Auto Update</label>
|
<!--<label class="control-label">Auto Update</label>-->
|
||||||
|
|
||||||
<div class="controls">
|
<!--<div class="controls">-->
|
||||||
<label class="checkbox toggle well">
|
<!--<label class="checkbox toggle well">-->
|
||||||
<input type="checkbox" name="autoUpdate"/>
|
<!--<input type="checkbox" name="autoUpdate"/>-->
|
||||||
|
|
||||||
<p>
|
<!--<p>-->
|
||||||
<span>Yes</span>
|
<!--<span>Yes</span>-->
|
||||||
<span>No</span>
|
<!--<span>No</span>-->
|
||||||
</p>
|
<!--</p>-->
|
||||||
|
|
||||||
<div class="btn btn-primary slide-button"/>
|
<!--<div class="btn btn-primary slide-button"/>-->
|
||||||
</label>
|
<!--</label>-->
|
||||||
|
|
||||||
<span class="help-inline-checkbox">
|
<!--<span class="help-inline-checkbox">-->
|
||||||
<i class="icon-nd-form-info" title="Use drone's built in auto update instead of package manager/manual updating"/>
|
<!--<i class="icon-nd-form-info" title="Use drone's built in auto update instead of package manager/manual updating"/>-->
|
||||||
</span>
|
<!--</span>-->
|
||||||
</div>
|
<!--</div>-->
|
||||||
</div>
|
<!--</div>-->
|
||||||
{{/if_mono}}
|
<!--{{/if_mono}}-->
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
@ -93,3 +93,11 @@ li.save-and-add:hover {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.api-key {
|
||||||
|
|
||||||
|
input {
|
||||||
|
width : 280px;
|
||||||
|
cursor : text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -27,6 +27,7 @@ require.config({
|
|||||||
'jquery.dotdotdot' : 'JsLibraries/jquery.dotdotdot',
|
'jquery.dotdotdot' : 'JsLibraries/jquery.dotdotdot',
|
||||||
'messenger' : 'JsLibraries/messenger',
|
'messenger' : 'JsLibraries/messenger',
|
||||||
'jquery' : 'JsLibraries/jquery',
|
'jquery' : 'JsLibraries/jquery',
|
||||||
|
'zero.clipboard' : 'JsLibraries/zero.clipboard',
|
||||||
'libs' : 'JsLibraries/',
|
'libs' : 'JsLibraries/',
|
||||||
|
|
||||||
'api': 'Require/require.api'
|
'api': 'Require/require.api'
|
||||||
|
Loading…
Reference in New Issue
Block a user