Commit 977b9714 authored by echel0n's avatar echel0n
Browse files

Switched WebUI to use API for login credentials

parent 39b4fd74
# Changelog
- * 9199b10 - 2018-06-16: Release v9.3.35
- * 10d61b1 - 2018-06-17: Switched WebUI to use API for login credentials
- * c800afc - 2018-06-16: Release v9.3.35
- * 2e05d78 - 2018-06-16: Added function to randomize external upnp port number Added ability to manually set external upnp port number
- * 8ee962a - 2018-06-16: Pre-Release v9.3.35.dev2
- * bcdcce3 - 2018-06-16: Pre-Release v9.3.35.dev1
......
......@@ -302,7 +302,7 @@ jQuery(document).ready(function ($) {
$('a.submiterrors').confirm({
title: gt('Submit Errors'),
content: gt('Are you sure you want to submit these errors ?<br><br><span class="red-text">Make sure SiCKRAGE is updated and trigger<br> this error with debug enabled before submitting</span>')
content: gt('Are you sure you want to submit these errors ?') + '<br><br><span class="red-text">' + gt('Make sure SiCKRAGE is updated and trigger') + '<br>' + gt('this error with debug enabled before submitting') + '</span>'
});
$('#removeW').click(function () {
......@@ -2020,7 +2020,7 @@ jQuery(document).ready(function ($) {
$("a.removeshow").confirm({
title: gt("Remove Show"),
content: gt('Are you sure you want to remove <span class="footerhighlight">') + $('#showtitle').data('showname') + gt('</span> from the database?<br><br><input type="checkbox" id="deleteFiles" name="deleteFiles"/>&nbsp;<label for="deleteFiles" class="red-text">Check to delete files as well. IRREVERSIBLE</label>'),
content: gt('Are you sure you want to remove') + '<span class="footerhighlight">' + $('#showtitle').data('showname') + '</span>' + gt(' from the database?') + '<br><br><input type="checkbox" id="deleteFiles" name="deleteFiles"/>&nbsp;<label for="deleteFiles" class="red-text">' + gt('Check to delete files as well. IRREVERSIBLE') + '</label>',
buttons: {
confirm: function () {
var $deleteFiles = this.$content.find('#deleteFiles').prop('checked');
......@@ -2245,7 +2245,7 @@ jQuery(document).ready(function ($) {
}
});
$('#tableDiv').html(gt('<img id="searchingAnim" src="' + SICKRAGE.srWebRoot + '/images/loading32.gif" height="32" width="32" /> loading folders...'));
$('#tableDiv').html('<img id="searchingAnim" src="' + SICKRAGE.srWebRoot + '/images/loading32.gif" height="32" width="32" />' + gt('loading folders...'));
$.get(SICKRAGE.srWebRoot + '/home/addShows/massAddTable/', url, function (data) {
$('#tableDiv').html(data);
$("#addRootDirTable").tablesorter({
......@@ -2541,11 +2541,11 @@ jQuery(document).ready(function ($) {
},
success: function (data) {
var firstResult = true;
var resultStr = gt('<legend class="legend">Search Results:</legend>\n');
var resultStr = '<legend class="legend">' + gt('Search Results:') + '</legend>\n';
var checked = '';
if (data.results.length === 0) {
resultStr += gt('<b>No results found, try a different search or language.</b>');
resultStr += '<b>' + gt('No results found, try a different search or language.') + '</b>';
} else {
$.each(data.results, function (index, obj) {
if (firstResult) {
......@@ -2564,9 +2564,9 @@ jQuery(document).ready(function ($) {
var startDate = new Date(obj[5]);
var today = new Date();
if (startDate > today) {
resultStr += gt(' (will debut on ' + obj[5] + ')');
resultStr += gt(' (will debut on ') + obj[5] + ')';
} else {
resultStr += gt(' (started on ' + obj[5] + ')');
resultStr += gt(' (started on ') + obj[5] + ')';
}
}
......@@ -2671,7 +2671,7 @@ jQuery(document).ready(function ($) {
beforeSubmit: function () {
$('.config_submitter .config_submitter_refresh').each(function () {
$(this).attr("disabled", "disabled");
$(this).after(gt('<span>' + SICKRAGE.loadingHTML + ' Saving...</span>'));
$(this).after('<span>' + SICKRAGE.loadingHTML + gt(' Saving...') + '</span>');
$(this).hide();
});
},
......@@ -2780,19 +2780,6 @@ jQuery(document).ready(function ($) {
});
});
$('#testAPI').click(function () {
$('#testAPI-result').html(SICKRAGE.loadingHTML);
var api_username = $.trim($('#api_username').val());
var api_password = $.trim($('#api_password').val());
$.get(SICKRAGE.srWebRoot + '/home/testAPI', {
'username': api_username,
'password': api_password
},
function (data) {
$('#testAPI-result').html(data);
});
});
$('#syncRemote').click(function () {
function updateProgress() {
$.getJSON(SICKRAGE.srWebRoot + '/googleDrive/getProgress', function (data) {
......@@ -3137,7 +3124,7 @@ jQuery(document).ready(function ($) {
} else if (selectedProvider.toLowerCase() === 'rtorrent') {
client = 'rTorrent';
$('#torrent_paused_option').hide();
$('#host_desc_torrent').html(gt('URL to your rTorrent client (e.g. scgi://localhost:5000 <br> or https://localhost/rutorrent/plugins/httprpc/action.php)'));
$('#host_desc_torrent').html(gt('URL to your rTorrent client (e.g. scgi://localhost:5000') + '<br>' + gt(' or https://localhost/rutorrent/plugins/httprpc/action.php)'));
$('#torrent_verify_cert_option').show();
$('#torrent_verify_deluge').hide();
$('#torrent_verify_rtorrent').show();
......@@ -4207,12 +4194,12 @@ jQuery(document).ready(function ($) {
pwd = $('#email_password').val();
err = '';
if (host === null) {
err += gt('<li style="color: red;">You must specify an SMTP hostname!</li>');
err += '<li style="color: red;">' + gt('You must specify an SMTP hostname!') + '</li>';
}
if (port === null) {
err += gt('<li style="color: red;">You must specify an SMTP port!</li>');
err += '<li style="color: red;">' + gt('You must specify an SMTP port!') + '</li>';
} else if (port.match(/^\d+$/) === null || parseInt(port, 10) > 65535) {
err += gt('<li style="color: red;">SMTP port must be between 0 and 65535!</li>');
err += '<li style="color: red;">' + gt('SMTP port must be between 0 and 65535!') + '</li>';
}
if (err.length > 0) {
err = '<ol>' + err + '</ol>';
......@@ -4220,7 +4207,7 @@ jQuery(document).ready(function ($) {
} else {
to = prompt(gt('Enter an email address to send the test to:'), null);
if (to === null || to.length === 0 || to.match(/.*@.*/) === null) {
status.html(gt('<p style="color: red;">You must provide a recipient email address!</p>'));
status.html('<p style="color: red;">' + gt('You must provide a recipient email address!') + '</p>');
} else {
$.get(SICKRAGE.srWebRoot + '/home/testEmail', {
host: host,
......@@ -5265,7 +5252,7 @@ jQuery(document).ready(function ($) {
$('.delete_root_dir').click(function () {
var curIndex = SICKRAGE.manage.mass_edit.findDirIndex($(this).attr('id'));
$('#new_root_dir_' + curIndex).val(null);
$('#display_new_root_dir_' + curIndex).html(gt('<b>DELETED</b>'));
$('#display_new_root_dir_' + curIndex).html('<b>' + gt('DELETED') + '</b>');
});
},
......
from __future__ import unicode_literals
import datetime
import json
import os
import sys
import time
from time import sleep
from urlparse import urljoin
from oauthlib.oauth2 import LegacyApplicationClient, MissingTokenError, InvalidClientIdError
......@@ -30,8 +27,8 @@ class API(object):
return {
'client_id': 2,
'client_secret': '7kEyr9jKuqOV4FFy2bOxOwA2RiB4WSHsEUU2P3BJ',
'username': self._username or sickrage.app.config.api_username,
'password': self._password or sickrage.app.config.api_password
'username': self._username or sickrage.app.config.app_username,
'password': self._password or sickrage.app.config.app_password,
}
@property
......@@ -53,9 +50,11 @@ class API(object):
if not os.path.exists(self.token_file):
oauth = OAuth2Session(client=LegacyApplicationClient(client_id=self.credentials['client_id']))
self._token = oauth.fetch_token(token_url=self.token_url,
timeout=30,
**self.credentials)
try:
self._token = oauth.fetch_token(token_url=self.token_url, timeout=30, **self.credentials)
except MissingTokenError:
self._token = None
self._token_updater(self._token)
else:
......@@ -69,9 +68,6 @@ class API(object):
json.dump(token, outfile)
def _request(self, method, url, **kwargs):
if not sickrage.app.config.enable_api:
return
try:
resp = self.session.request(method, urljoin(self.api_url, url), timeout=30,
hooks={'response': self.throttle_hook}, **kwargs)
......@@ -83,8 +79,7 @@ class API(object):
return resp.json()
except (InvalidClientIdError, MissingTokenError) as e:
sickrage.app.log.warning("Token is required for interacting with SiCKRAGE API, check username/password "
"under General settings.")
sickrage.app.log.warning("SiCKRAGE username or password is incorrect, please try again")
@staticmethod
def throttle_hook(response, **kwargs):
......
......@@ -61,10 +61,7 @@ class Config(object):
self.log_size = 1048576
self.log_nr = 5
self.enable_api = False
self.enable_api_providers_cache = False
self.api_username = ""
self.api_password = ""
self.enable_upnp = True
self.version_notify = True
......@@ -84,8 +81,8 @@ class Config(object):
self.web_external_port = random.randint(49152, 65536)
self.web_log = False
self.web_root = ""
self.web_username = ""
self.web_password = ""
self.app_username = ""
self.app_password = ""
self.web_ipv6 = False
self.web_cookie_secret = ""
self.web_use_gzip = True
......@@ -693,10 +690,7 @@ class Config(object):
},
'General': {
'app_id': uuid.uuid4(),
'enable_api': True,
'enable_api_providers_cache': True,
'api_username': '',
'api_password': '',
'log_size': 1048576,
'calendar_unprotected': False,
'https_key': os.path.abspath(os.path.join(sickrage.PROG_DIR, 'server.key')),
......@@ -742,7 +736,8 @@ class Config(object):
'naming_pattern': 'Season %0S/%SN - S%0SE%0E - %EN',
'sort_article': False,
'handle_reverse_proxy': False,
'web_username': '',
'app_username': '',
'app_password': '',
'postpone_if_sync_files': True,
'cpu_preset': 'NORMAL',
'nfo_rename': True,
......@@ -807,7 +802,6 @@ class Config(object):
'log_nr': 5,
'git_newver': False,
'git_reset': True,
'web_password': '',
'scene_default': False,
'skip_removed_files': False,
'status_default_after': WANTED,
......@@ -1369,10 +1363,7 @@ class Config(object):
# GENERAL SETTINGS
self.config_version = self.check_setting_int('General', 'config_version')
self.app_id = self.check_setting_str('General', 'app_id')
self.enable_api = self.check_setting_bool('General', 'enable_api')
self.enable_api_providers_cache = self.check_setting_bool('General', 'enable_api_providers_cache')
self.api_username = self.check_setting_str('General', 'api_username', censor=True)
self.api_password = self.check_setting_str('General', 'api_password', censor=True)
self.debug = sickrage.app.debug or self.check_setting_bool('General', 'debug')
self.last_db_compact = self.check_setting_int('General', 'last_db_compact')
self.log_nr = self.check_setting_int('General', 'log_nr')
......@@ -1392,8 +1383,8 @@ class Config(object):
self.web_ipv6 = self.check_setting_bool('General', 'web_ipv6')
self.web_root = self.check_setting_str('General', 'web_root').rstrip("/")
self.web_log = self.check_setting_bool('General', 'web_log')
self.web_username = self.check_setting_str('General', 'web_username', censor=True)
self.web_password = self.check_setting_str('General', 'web_password', censor=True)
self.app_username = self.check_setting_str('General', 'app_username', censor=True)
self.app_password = self.check_setting_str('General', 'app_password', censor=True)
self.web_cookie_secret = self.check_setting_str('General', 'web_cookie_secret')
self.web_use_gzip = self.check_setting_bool('General', 'web_use_gzip')
self.ssl_verify = self.check_setting_bool('General', 'ssl_verify')
......@@ -1867,10 +1858,7 @@ class Config(object):
'encryption_secret': self.encryption_secret,
'last_db_compact': self.last_db_compact,
'app_id': self.app_id,
'enable_api': int(self.enable_api),
'enable_api_providers_cache': int(self.enable_api_providers_cache),
'api_username': self.api_username,
'api_password': self.api_password,
'git_autoissues': int(self.git_autoissues),
'git_username': self.git_username,
'git_password': self.git_password,
......@@ -1885,8 +1873,8 @@ class Config(object):
'web_ipv6': int(self.web_ipv6),
'web_log': int(self.web_log),
'web_root': self.web_root,
'web_username': self.web_username,
'web_password': self.web_password,
'app_username': self.app_username,
'app_password': self.app_password,
'web_cookie_secret': self.web_cookie_secret,
'web_use_gzip': int(self.web_use_gzip),
'ssl_verify': int(self.ssl_verify),
......
......@@ -32,6 +32,7 @@ from tornado.escape import json_encode, recursive_unicode
from tornado.web import RequestHandler
import sickrage.subtitles
from sickrage.core import API
try:
from futures import ThreadPoolExecutor
......@@ -87,11 +88,11 @@ class KeyHandler(RequestHandler):
api_key = None
try:
username = sickrage.app.config.web_username
password = sickrage.app.config.web_password
username = self.get_argument('u', None)
password = self.get_argument('p', None)
api_token = API(username, password).token
if (self.get_argument('u', None) == username or not username) and \
(self.get_argument('p', None) == password or not password):
if api_token:
api_key = sickrage.app.config.api_key
self.finish({'success': api_key is not None, 'api_key': api_key})
......
......@@ -35,7 +35,6 @@ from CodernityDB.database import RecordNotFound
from concurrent.futures import ThreadPoolExecutor
from mako.exceptions import RichTraceback
from mako.lookup import TemplateLookup
from oauthlib.oauth2 import MissingTokenError
from tornado.concurrent import run_on_executor
from tornado.escape import json_encode, recursive_unicode
from tornado.gen import coroutine
......@@ -160,9 +159,15 @@ class BaseHandler(RequestHandler):
super(BaseHandler, self).redirect(url, permanent, status)
def set_current_user(self, user, remember_me=False):
self.set_secure_cookie('sickrage_user', user, expires_days=30 if remember_me else None)
def get_current_user(self):
return self.get_secure_cookie('sickrage_user')
def clear_current_user(self):
self.clear_cookie('sickrage_user')
def render_string(self, template_name, **kwargs):
template_kwargs = {
'title': "",
......@@ -250,6 +255,7 @@ class WebHandler(BaseHandler):
action='genericmessage'
)
class LoginHandler(BaseHandler):
def __init__(self, *args, **kwargs):
super(LoginHandler, self).__init__(*args, **kwargs)
......@@ -258,28 +264,27 @@ class LoginHandler(BaseHandler):
self.finish(self.auth())
def auth(self):
if sickrage.app.developer:
self.set_secure_cookie('sickrage_user', json_encode(sickrage.app.config.api_key))
if self.get_current_user():
return self.redirect("/{}/".format(sickrage.app.config.default_page))
username = self.get_argument('username', '')
password = self.get_argument('password', '')
api_token = API(username, password).token
if username == sickrage.app.config.web_username and password == sickrage.app.config.web_password:
Notifiers.mass_notify_login(self.request.remote_ip)
remember_me = int(self.get_argument('remember_me', default=0))
if all([username, password]) and api_token:
sickrage.app.config.app_username = username
sickrage.app.config.app_password = password
sickrage.app.config.save()
self.set_secure_cookie('sickrage_user', json_encode(sickrage.app.config.api_key),
expires_days=30 if remember_me else None)
remember_me = bool(self.get_argument('remember_me', default=0))
self.set_current_user(json_encode(api_token), remember_me)
Notifiers.mass_notify_login(self.request.remote_ip)
sickrage.app.log.info('User logged into the SiCKRAGE web interface')
redirect_page = self.get_argument('next', "/{}/".format(sickrage.app.config.default_page))
return self.redirect("{}".format(redirect_page))
elif username and password:
elif all([username, password]):
sickrage.app.log.warning(
'User attempted a failed login to the SiCKRAGE web interface from IP: {}'.format(
self.request.remote_ip)
......@@ -300,7 +305,7 @@ class LogoutHandler(BaseHandler):
super(LogoutHandler, self).__init__(*args, **kwargs)
def prepare(self, *args, **kwargs):
self.clear_cookie("sickrage_user")
self.clear_current_user()
return self.redirect('/login/')
......@@ -710,16 +715,6 @@ class Home(WebHandler):
else:
return False
@staticmethod
def testAPI(username=None, password=None):
try:
if API(username, password).token:
return _('API access successful')
except MissingTokenError:
pass
return _('API access failed')
@staticmethod
def testSABnzbd(host=None, username=None, password=None, apikey=None):
host = clean_url(host)
......@@ -3780,8 +3775,8 @@ class ConfigGeneral(Config):
def saveGeneral(self, log_dir=None, log_nr=5, log_size=1048576, web_port=None, web_log=None,
encryption_version=None, web_ipv6=None, trash_remove_show=None, trash_rotate_logs=None,
update_frequency=None, skip_removed_files=None, indexerDefaultLang='en',
ep_default_deleted_status=None, launch_browser=None, showupdate_hour=3, web_username=None,
api_key=None, indexer_default=None, timezone_display=None, cpu_preset='NORMAL', web_password=None,
ep_default_deleted_status=None, launch_browser=None, showupdate_hour=3,
api_key=None, indexer_default=None, timezone_display=None, cpu_preset='NORMAL',
version_notify=None, enable_https=None, https_cert=None, https_key=None, handle_reverse_proxy=None,
sort_article=None, auto_update=None, notify_on_update=None, proxy_setting=None, proxy_indexers=None,
anon_redirect=None, git_path=None, pip_path=None, calendar_unprotected=None, calendar_icons=None,
......@@ -3789,17 +3784,13 @@ class ConfigGeneral(Config):
fuzzy_dating=None, trim_zero=None, date_preset=None, date_preset_na=None, time_preset=None,
indexer_timeout=None, download_url=None, rootDir=None, theme_name=None, default_page=None,
git_reset=None, git_username=None, git_password=None, git_autoissues=None, gui_language=None,
display_all_seasons=None, showupdate_stale=None, notify_on_login=None, api_username=None,
api_password=None, use_api=None, enable_api_providers_cache=None, enable_upnp=None,
web_external_port=None, **kwargs):
display_all_seasons=None, showupdate_stale=None, notify_on_login=None,
enable_api_providers_cache=None, enable_upnp=None, web_external_port=None, **kwargs):
results = []
# API
sickrage.app.config.enable_api = checkbox_to_value(use_api)
sickrage.app.config.enable_api_providers_cache = checkbox_to_value(enable_api_providers_cache)
sickrage.app.config.api_username = api_username
sickrage.app.config.api_password = api_password
# Language
sickrage.app.config.change_gui_lang(gui_language)
......@@ -3850,8 +3841,6 @@ class ConfigGeneral(Config):
sickrage.app.config.web_port = try_int(web_port)
sickrage.app.config.web_ipv6 = checkbox_to_value(web_ipv6)
sickrage.app.config.encryption_version = (0, 2)[checkbox_to_value(encryption_version) == 1]
sickrage.app.config.web_username = web_username
sickrage.app.config.web_password = web_password
sickrage.app.config.filter_row = checkbox_to_value(filter_row)
sickrage.app.config.fuzzy_dating = checkbox_to_value(fuzzy_dating)
......
......@@ -30,116 +30,54 @@
<div class="row tab-pane">
<div class="col-lg-3 col-md-4 col-sm-4 col-xs-12 tab-pane-desc">
<h3>${_('SiCKRAGE API')}</h3>
<p>${_('Credentials and options for api.sickrage.ca')}</p>
<p>${_('Options for api.sickrage.ca')}</p>
</div>
<fieldset class="col-lg-9 col-md-8 col-sm-8 col-xs-12 tab-pane-list">
<div class="row field-pair">
<div class="col-lg-3 col-md-4 col-sm-5 col-xs-12">
<label class="component-title">${_('Enable')}</label>
<label class="component-title">${_('API Provider Cache')}</label>
</div>
<div class="col-lg-9 col-md-8 col-sm-7 col-xs-12 component-desc">
<input type="checkbox" class="enabler" name="use_api"
id="use_api" ${('', 'checked')[bool(sickrage.app.config.enable_api)]}/>
<label for="use_api">${_('Enable API access ?')}</label>
<div class="row">
<div class="col-md-12">
<a href="${anon_url('https://api.sickrage.ca/')}" rel="noreferrer"
onclick="window.open(this.href, '_blank'); return false;">${_('Register for API access')}</a></h3>
</div>
</div>
<input type="checkbox" class="enabler" name="enable_api_providers_cache"
id="enable_api_providers_cache" ${('', 'checked')[bool(sickrage.app.config.enable_api_providers_cache)]}/>
<label for="enable_api_providers_cache">${_('Enable provider cache ?')}</label>
</div>
</div>
<div id="content_use_api">
<div class="row field-pair">
<div class="col-lg-3 col-md-4 col-sm-5 col-xs-12">
<label class="component-title">${_('API Provider Cache')}</label>
</div>
<div class="col-lg-9 col-md-8 col-sm-7 col-xs-12 component-desc">
<input type="checkbox" class="enabler" name="enable_api_providers_cache"
id="enable_api_providers_cache" ${('', 'checked')[bool(sickrage.app.config.enable_api_providers_cache)]}/>
<label for="enable_api_providers_cache">${_('Enable provider cache ?')}</label>
</div>
<div class="row field-pair">
<div class="col-lg-3 col-md-4 col-sm-5 col-xs-12">
<label class="component-title">${_('Google Drive')}</label>
</div>
<div class="row field-pair">
<div class="col-lg-3 col-md-4 col-sm-5 col-xs-12">
<label class="component-title">${_('API Username')}</label>
</div>
<div class="col-lg-9 col-md-8 col-sm-7 col-xs-12 component-desc">
<div class="input-group input350">
<div class="input-group-addon">
<span class="glyphicon glyphicon-user"></span>
<div class="col-lg-9 col-md-8 col-sm-7 col-xs-12 component-desc">
% try:
% if GoogleDriveAPI().is_connected()['success']:
<div class="row">
<div class="col-md-12">
<span class="label label-success">CONNECTED</span>
</div>
<input name="api_username" id="api_username"
value="${sickrage.app.config.api_username}"
title="API Username"
class="form-control"
autocapitalize="off"/>
</div>
</div>
</div>
<div class="row field-pair">
<div class="col-lg-3 col-md-4 col-sm-5 col-xs-12">
<label class="component-title">${_('API Password')}</label>
</div>
<div class="col-lg-9 col-md-8 col-sm-7 col-xs-12 component-desc">
<div class="input-group input350">
<div class="input-group-addon">
<span class="glyphicon glyphicon-lock"></span>
<br/>
<div class="row">
<div class="col-md-12">
<input class="btn" type="button" value="${_('Sync To Google Drive')}"
id="syncRemote"/>
<input class="btn" type="button" value="${_('Sync To Local Drive')}"
id="syncLocal"/>
</div>
<input type="password" name="api_password" id="api_password"
value="${sickrage.app.config.api_password}"
title="API Password"
class="form-control"
autocapitalize="off"/>
</div>
</div>
</div>
<div class="row field-pair">
<div class="col-lg-3 col-md-4 col-sm-5 col-xs-12">
<label class="component-title">${_('Google Drive')}</label>
</div>
<div class="col-lg-9 col-md-8 col-sm-7 col-xs-12 component-desc">
% try:
% if GoogleDriveAPI().is_connected()['success']:
<div class="row">
<div class="col-md-12">
<span class="label label-success">CONNECTED</span>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<input class="btn" type="button" value="${_('Sync To Google Drive')}"
id="syncRemote"/>
<input class="btn" type="button" value="${_('Sync To Local Drive')}"
id="syncLocal"/>
</div>
</div>
% else:
<span class="label label-danger">DISCONNECTED</span>
% endif
% except Exception:
% else:
<span class="label label-danger">DISCONNECTED</span>
% endtry:
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="testNotification" id="testAPI-result">${_('Click below to test.')}</div>
</div>
% endif
% except Exception:
<span class="label label-danger">DISCONNECTED</span>
% endtry:
</div>
</div>
<div class="row">
<div class="col-md-12">
<input class="btn" type="button" value="${_('Test API')}" id="testAPI"/>
<input type="submit" class="btn config_submitter" value="${_('Save Changes')}"/>
</div>
<div class="row">
<div class="col-md-12">
<input type="submit" class="btn config_submitter" value="${_('Save Changes')}"/>
</div>
</div>
</fieldset>
......@@ -779,35 +717,31 @@
<div class="row field-pair">
<div class="col-lg-3 col-md-4 col-sm-5 col-xs-12">
<label class="component-title">${_('API key')}</label>
<label class="component-title">${_('HTTP private port')}</label>
</div>
<div class="col-lg-9 col-md-8 col-sm-7 col-xs-12 component-desc">
<div class="row">
<div class="col-md-12">
<div class="input-group input350">
<div class="input-group-addon">
<span class="glyphicon glyphicon-cloud"></span>
<span class="glyphicon glyphicon-globe"></span>
</div>
<input name="api_key" id="api_key"