Merge pull request #11707 from Atlante45/feat/wizard

Domain Settings tweaks + Domain Setup Wizard
This commit is contained in:
Stephen Birarda 2017-10-30 08:39:31 -07:00 committed by GitHub
commit 05cfd72464
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 2369 additions and 362 deletions

View file

@ -1,6 +1,12 @@
{
"version": 1.9,
"version": 2.0,
"settings": [
{
"name": "label",
"label": "Label",
"settings": [
]
},
{
"name": "metaverse",
"label": "Metaverse / Networking",
@ -14,7 +20,8 @@
{
"name": "id",
"label": "Domain ID",
"help": "This is your High Fidelity domain ID. If you do not want your domain to be registered in the High Fidelity metaverse you can leave this blank."
"help": "This is your High Fidelity domain ID. If you do not want your domain to be registered in the High Fidelity metaverse you can leave this blank.",
"advanced": true
},
{
"name": "automatic_networking",
@ -82,11 +89,13 @@
{
"name": "description",
"label": "Description",
"advanced": true,
"help": "A description of your domain (256 character limit)."
},
{
"name": "maturity",
"label": "Maturity",
"advanced": true,
"help": "A maturity rating, available as a guideline for content on your domain.",
"default": "unrated",
"type": "select",
@ -116,6 +125,7 @@
{
"name": "hosts",
"label": "Hosts",
"advanced": true,
"type": "table",
"can_add_new_rows": true,
"help": "Usernames of hosts who can reliably show your domain to new visitors.",
@ -131,6 +141,7 @@
{
"name": "tags",
"label": "Tags",
"advanced": true,
"type": "table",
"can_add_new_rows": true,
"help": "Common categories under which your domain falls.",
@ -1625,6 +1636,29 @@
]
}
]
},
{
"name": "wizard",
"label": "Setup Wizard",
"restart": false,
"hidden": true,
"settings": [
{
"name": "cloud_domain",
"type": "checkbox",
"default": false
},
{
"name": "steps_completed",
"type": "int",
"default": 0
},
{
"name": "completed_once",
"type": "checkbox",
"default": false
}
]
}
]
}
}

View file

@ -1,6 +1,7 @@
body {
position: relative;
padding-bottom: 30px;
margin-top: 70px;
}
[hidden] {
@ -27,14 +28,14 @@ body {
.table .value-row td,
.table .value-category td,
.table .inputs td {
vertical-align: middle;
vertical-align: middle;
}
.table .table-checkbox {
/* Fix IE sizing checkboxes to fill table cell */
width: auto;
margin-left: auto;
margin-right: auto;
/* Fix IE sizing checkboxes to fill table cell */
width: auto;
margin-left: auto;
margin-right: auto;
}
.value-category:not(.inputs) {
@ -80,8 +81,10 @@ span.port {
}
#setup-sidebar.affix {
position: fixed;
top: 15px;
/* This overrides a case where going to the bottom of the page,
* then scrolling up, causes `position: relative` to be added to the style
*/
position: fixed !important;
}
#setup-sidebar button {
@ -145,55 +148,55 @@ table {
}
caption {
color: #333;
font-weight: 700;
padding-top: 0;
color: #333;
font-weight: 700;
padding-top: 0;
}
table > tbody > .headers > td {
vertical-align: middle;
vertical-align: middle;
}
table .headers + .headers td {
font-size: 13px;
color: #222;
font-size: 13px;
color: #222;
}
#security table .headers td + td {
text-align: center;
text-align: center;
}
.tooltip.top .tooltip-arrow {
border-top-color: #fff;
border-width: 10px 10px 0;
margin-bottom: -5px;
border-top-color: #fff;
border-width: 10px 10px 0;
margin-bottom: -5px;
}
.tooltip-inner {
padding: 20px 20px 10px 20px;
font-size: 14px;
text-align: left;
color: #333;
background-color: #fff;
box-shadow: 0 3px 8px 8px #e8e8e8;
padding: 20px 20px 10px 20px;
font-size: 14px;
text-align: left;
color: #333;
background-color: #fff;
box-shadow: 0 3px 8px 8px #e8e8e8;
}
.tooltip.in {
opacity: 1;
opacity: 1;
}
.tooltip-inner ul {
padding-left: 0;
margin-bottom: 15px;
padding-left: 0;
margin-bottom: 15px;
}
.tooltip-inner li {
list-style-type: none;
margin-bottom: 5px;
list-style-type: none;
margin-bottom: 5px;
}
#security .tooltip-inner {
max-width: 520px;
max-width: 520px;
}
#xs-advanced-container {
@ -241,6 +244,20 @@ table .headers + .headers td {
animation-delay: -0.16s;
}
.col-centered {
float: none;
margin: 0 auto;
}
.centered-hack-parent {
text-align: center;
}
.centered-hack {
text-align: left;
display: inline-block;
}
@-webkit-keyframes bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0.0) }
40% { -webkit-transform: scale(1.0) }
@ -255,3 +272,50 @@ table .headers + .headers td {
-webkit-transform: scale(1.0);
}
}
/* From https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d */
.glyphicon-refresh-animate {
-animation: spin .7s infinite linear;
-ms-animation: spin .7s infinite linear;
-webkit-animation: spinw .7s infinite linear;
-moz-animation: spinm .7s infinite linear;
}
@keyframes spin {
from { transform: scale(1) rotate(0deg); }
to { transform: scale(1) rotate(360deg); }
}
@-webkit-keyframes spinw {
from { -webkit-transform: rotate(0deg); }
to { -webkit-transform: rotate(360deg); }
}
@-moz-keyframes spinm {
from { -moz-transform: rotate(0deg); }
to { -moz-transform: rotate(360deg); }
}
.warning-text {
padding-top: 10px;
color: #EB5757;
}
.account-connected-header {
color: #6FCF97;
font-size: 30px;
margin-right: 20px;
}
#visit-domain-link,
.blue-link {
font-size: 14px;
text-decoration-line: underline;
font-weight: normal;
color: #2F80ED;
}
#manage-cloud-domains-link {
text-align: center;
margin-top: 20px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -13,7 +13,7 @@
<script src='/js/sweetalert.min.js'></script>
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
@ -23,7 +23,6 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">domain-server</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
@ -40,6 +39,7 @@
<li><a href="/settings/">Settings</a></li>
</ul>
<ul class="nav navbar-right navbar-nav">
<li><a id="visit-domain-link" target="_blank" style="display: none;">Visit domain in VR</a></li>
<li><a href="#" id="restart-server"><span class="glyphicon glyphicon-refresh"></span> Restart</a></li>
</ul>
</div>

View file

@ -0,0 +1,25 @@
<svg width="676" height="676" viewBox="0 0 676 676" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>CongratulationImage</title>
<desc>Created using Figma</desc>
<g id="Canvas" transform="matrix(4 0 0 4 -21208 -17980)">
<g id="CongratulationImage">
<g id="Ellipse">
<use xlink:href="#path0_fill" transform="translate(5302 4495)" fill="#FFFFFF"/>
<mask id="mask0_outline_ins">
<use xlink:href="#path0_fill" fill="white" transform="translate(5302 4495)"/>
</mask>
<g mask="url(#mask0_outline_ins)">
<use xlink:href="#path1_stroke_2x" transform="translate(5302 4495)" fill="#219653"/>
</g>
</g>
<g id="Vector 2">
<use xlink:href="#path2_stroke" transform="translate(5355 4559)" fill="#219653"/>
</g>
</g>
</g>
<defs>
<path id="path0_fill" d="M 169 84.5C 169 131.168 131.168 169 84.5 169C 37.8319 169 0 131.168 0 84.5C 0 37.8319 37.8319 0 84.5 0C 131.168 0 169 37.8319 169 84.5Z"/>
<path id="path1_stroke_2x" d="M 154 84.5C 154 122.884 122.884 154 84.5 154L 84.5 184C 139.452 184 184 139.452 184 84.5L 154 84.5ZM 84.5 154C 46.1162 154 15 122.884 15 84.5L -15 84.5C -15 139.452 29.5477 184 84.5 184L 84.5 154ZM 15 84.5C 15 46.1162 46.1162 15 84.5 15L 84.5 -15C 29.5477 -15 -15 29.5477 -15 84.5L 15 84.5ZM 84.5 15C 122.884 15 154 46.1162 154 84.5L 184 84.5C 184 29.5477 139.452 -15 84.5 -15L 84.5 15Z"/>
<path id="path2_stroke" d="M 5.18747 19.8031C 2.19593 16.9382 -2.5517 17.0408 -5.41666 20.0323C -8.28162 23.0238 -8.17901 27.7715 -5.18747 30.6364L 5.18747 19.8031ZM 20.6541 45L 15.4667 50.4167C 18.3816 53.2083 22.9831 53.1924 25.8787 50.3809L 20.6541 45ZM 72.2246 5.38085C 75.1964 2.49539 75.2663 -2.25283 72.3809 -5.2246C 69.4954 -8.19636 64.7472 -8.26632 61.7754 -5.38085L 72.2246 5.38085ZM -5.18747 30.6364L 15.4667 50.4167L 25.8416 39.5833L 5.18747 19.8031L -5.18747 30.6364ZM 25.8787 50.3809L 72.2246 5.38085L 61.7754 -5.38085L 15.4295 39.6191L 25.8787 50.3809Z"/>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -32,7 +32,6 @@ $(document).ready(function(){
$('ul.nav a').filter(function() {
return this.href == url;
}).parent().addClass('active');
$('body').on('click', '#restart-server', function(e) {
swal( {
title: "Are you sure?",
@ -46,4 +45,4 @@ $(document).ready(function(){
});
return false;
});
});
});

View file

@ -0,0 +1,393 @@
var Settings = {
showAdvanced: false,
ADVANCED_CLASS: 'advanced-setting',
DEPRECATED_CLASS: 'deprecated-setting',
TRIGGER_CHANGE_CLASS: 'trigger-change',
DATA_ROW_CLASS: 'value-row',
DATA_COL_CLASS: 'value-col',
DATA_CATEGORY_CLASS: 'value-category',
ADD_ROW_BUTTON_CLASS: 'add-row',
ADD_ROW_SPAN_CLASSES: 'glyphicon glyphicon-plus add-row',
DEL_ROW_BUTTON_CLASS: 'del-row',
DEL_ROW_SPAN_CLASSES: 'glyphicon glyphicon-remove del-row',
ADD_CATEGORY_BUTTON_CLASS: 'add-category',
ADD_CATEGORY_SPAN_CLASSES: 'glyphicon glyphicon-plus add-category',
TOGGLE_CATEGORY_COLUMN_CLASS: 'toggle-category',
TOGGLE_CATEGORY_SPAN_CLASS: 'toggle-category-icon',
TOGGLE_CATEGORY_SPAN_CLASSES: 'glyphicon toggle-category-icon',
TOGGLE_CATEGORY_EXPANDED_CLASS: 'glyphicon-triangle-bottom',
TOGGLE_CATEGORY_CONTRACTED_CLASS: 'glyphicon-triangle-right',
DEL_CATEGORY_BUTTON_CLASS: 'del-category',
DEL_CATEGORY_SPAN_CLASSES: 'glyphicon glyphicon-remove del-category',
MOVE_UP_BUTTON_CLASS: 'move-up',
MOVE_UP_SPAN_CLASSES: 'glyphicon glyphicon-chevron-up move-up',
MOVE_DOWN_BUTTON_CLASS: 'move-down',
MOVE_DOWN_SPAN_CLASSES: 'glyphicon glyphicon-chevron-down move-down',
TABLE_BUTTONS_CLASS: 'buttons',
ADD_DEL_BUTTONS_CLASS: 'add-del-buttons',
ADD_DEL_BUTTONS_CLASSES: 'buttons add-del-buttons',
REORDER_BUTTONS_CLASS: 'reorder-buttons',
REORDER_BUTTONS_CLASSES: 'buttons reorder-buttons',
NEW_ROW_CLASS: 'new-row',
CONNECT_ACCOUNT_BTN_ID: 'connect-account-btn',
DISCONNECT_ACCOUNT_BTN_ID: 'disconnect-account-btn',
CREATE_DOMAIN_ID_BTN_ID: 'create-domain-btn',
CHOOSE_DOMAIN_ID_BTN_ID: 'choose-domain-btn',
GET_TEMPORARY_NAME_BTN_ID: 'get-temp-name-btn',
DOMAIN_ID_SELECTOR: '[name="metaverse.id"]',
ACCESS_TOKEN_SELECTOR: '[name="metaverse.access_token"]',
PLACES_TABLE_ID: 'places-table',
ADD_PLACE_BTN_ID: 'add-place-btn',
FORM_ID: 'settings-form',
INVALID_ROW_CLASS: 'invalid-input',
DATA_ROW_INDEX: 'data-row-index'
};
var URLs = {
// STABLE METAVERSE_URL: https://metaverse.highfidelity.com
// STAGING METAVERSE_URL: https://staging.highfidelity.com
METAVERSE_URL: 'https://metaverse.highfidelity.com',
PLACE_URL: 'https://hifi.place',
};
var Strings = {
LOADING_SETTINGS_ERROR: "There was a problem loading the domain settings.\nPlease refresh the page to try again.",
CHOOSE_DOMAIN_BUTTON: "Choose from my domains",
CREATE_DOMAIN_BUTTON: "Create new domain ID",
CREATE_DOMAIN_SUCCESS_JUST_CONNECTED: "We connnected your High Fidelity account and created a new domain ID for this machine.",
CREATE_DOMAIN_SUCCESS: "We created a new domain ID for this machine.",
// When a place modification fails, they will be brought back to the previous
// dialog with new path still set, allowing them to retry immediately, and without
// having to type the new path in again.
EDIT_PLACE_TITLE: "Modify Viewpoint or Path",
EDIT_PLACE_ERROR: "Failed to update place path. Please try again.",
EDIT_PLACE_CONFIRM_BUTTON: "Save",
EDIT_PLACE_CONFIRM_BUTTON_PENDING: "Saving...",
EDIT_PLACE_CANCEL_BUTTON: "Cancel",
REMOVE_PLACE_TITLE: "Are you sure you want to remove <strong>{{place}}</strong>?",
REMOVE_PLACE_ERROR: "Failed to remove place. Please try again.",
REMOVE_PLACE_DELETE_BUTTON: "Delete",
REMOVE_PLACE_DELETE_BUTTON_PENDING: "Deleting...",
REMOVE_PLACE_CANCEL_BUTTON: "Cancel",
ADD_PLACE_TITLE: "Choose a place",
ADD_PLACE_MESSAGE: "Choose the High Fidelity place to point at this domain server.",
ADD_PLACE_CONFIRM_BUTTON: "Choose place",
ADD_PLACE_CONFIRM_BUTTON_PENDING: "Saving...",
ADD_PLACE_CANCEL_BUTTON: "Cancel",
ADD_PLACE_UNKNOWN_ERROR: "There was an error adding this place name.",
ADD_PLACE_NO_PLACES_MESSAGE: "<p>You do not have any places in your High Fidelity account."
+ "<br/><br/>Go to your <a href='https://metaverse.highfidelity.com/user/places/new'>places page</a> to create a new one. Once your place is created re-open this dialog to select it.</p>",
ADD_PLACE_NO_PLACES_BUTTON: "Create new place",
ADD_PLACE_UNABLE_TO_LOAD_ERROR: "We were unable to load your place names. Please try again later.",
ADD_PLACE_LOADING_DIALOG: "Loading your places...",
ADD_PLACE_NOT_CONNECTED_TITLE: "Access token required",
ADD_PLACE_NOT_CONNECTED_MESSAGE: "You must have an access token to query your High Fidelity places.<br><br>Please follow the instructions on the settings page to add an access token.",
};
var DOMAIN_ID_TYPE_NONE = 0;
var DOMAIN_ID_TYPE_TEMP = 1;
var DOMAIN_ID_TYPE_FULL = 2;
var DOMAIN_ID_TYPE_UNKNOWN = 3;
function domainIDIsSet() {
return Settings.data.values.metaverse.id.length > 0;
}
function getCurrentDomainIDType() {
if (!domainIDIsSet()) {
return DOMAIN_ID_TYPE_NONE;
}
if (typeof DomainInfo === 'undefined') {
return DOMAIN_ID_TYPE_UNKNOWN;
}
if (DomainInfo !== null) {
if (DomainInfo.name !== undefined) {
return DOMAIN_ID_TYPE_TEMP;
}
return DOMAIN_ID_TYPE_FULL;
}
return DOMAIN_ID_TYPE_UNKNOWN;
}
function showLoadingDialog(msg) {
var message = '<div class="text-center">';
message += '<span class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></span> ' + msg;
message += '</div>';
return bootbox.dialog({
message: message,
closeButton: false
});
}
function sendUpdatePlaceRequest(id, path, domainID, clearDomainID, onSuccess, onError) {
var data = {
place_id: id,
path: path
};
if (domainID) {
data.domain_id = domainID;
}
if (clearDomainID) {
data.domain_id = null;
}
$.ajax({
url: '/api/places',
type: 'PUT',
data: data,
success: onSuccess,
error: onError
});
}
function chooseFromHighFidelityPlaces(accessToken, forcePathTo, onSuccessfullyAdded) {
if (accessToken) {
var loadingDialog = showLoadingDialog(Strings.ADD_PLACE_LOADING_DIALOG);
$.ajax("/api/places", {
dataType: 'json',
jsonp: false,
success: function(data) {
if (data.status == 'success') {
var modal_buttons = {
cancel: {
label: Strings.ADD_PLACE_CANCEL_BUTTON,
className: 'add-place-cancel-button btn-default'
}
};
var dialog;
var modal_body;
if (data.data.places.length) {
var places_by_id = {};
modal_body = $('<div>');
modal_body.append($("<p>Choose a place name that you own or <a href='" + URLs.METAVERSE_URL + "/user/places' target='_blank'>register a new place name</a></p>"));
var currentDomainIDType = getCurrentDomainIDType();
if (currentDomainIDType === DOMAIN_ID_TYPE_TEMP) {
var warning = "<div class='domain-loading-error alert alert-warning'>";
warning += "If you choose a place name it will replace your current temporary place name.";
warning += "</div>";
modal_body.append(warning);
}
// setup a select box for the returned places
modal_body.append($("<label for='place-name-select'>Places</label>"));
place_select = $("<select id='place-name-select' class='form-control'></select>");
_.each(data.data.places, function(place) {
places_by_id[place.id] = place;
place_select.append("<option value='" + place.id + "'>" + place.name + "</option>");
})
modal_body.append(place_select);
modal_body.append($("<p id='place-name-warning' class='warning-text' style='display: none'>This place name already points to a place or path. Saving this would overwrite the previous settings associated with it.</p>"));
if (forcePathTo === undefined || forcePathTo === null) {
var path = "<div class='form-group'>";
path += "<label for='place-path-input' class='control-label'>Path</label>";
path += "<input type='text' id='place-path-input' class='form-control' value='/'>";
path += "</div>";
modal_body.append($(path));
}
var place_select = modal_body.find("#place-name-select")
place_select.change(function(ev) {
var warning = modal_body.find("#place-name-warning");
var place = places_by_id[$(this).val()];
if (place === undefined || place.pointee === null) {
warning.hide();
} else {
warning.show();
}
});
place_select.trigger('change');
modal_buttons["success"] = {
label: Strings.ADD_PLACE_CONFIRM_BUTTON,
className: 'add-place-confirm-button btn btn-primary',
callback: function() {
var placeID = $('#place-name-select').val();
// set the place ID on the form
$(Settings.place_ID_SELECTOR).val(placeID).change();
if (forcePathTo === undefined || forcePathTo === null) {
var placePath = $('#place-path-input').val();
} else {
var placePath = forcePathTo;
}
$('.add-place-confirm-button').attr('disabled', 'disabled');
$('.add-place-confirm-button').html(Strings.ADD_PLACE_CONFIRM_BUTTON_PENDING);
$('.add-place-cancel-button').attr('disabled', 'disabled');
function finalizeSaveDomainID(domainID) {
var jsonSettings = {
metaverse: {
id: domainID
}
}
var dialog = showLoadingDialog("Waiting for Domain Server to restart...");
$.ajax('/settings.json', {
data: JSON.stringify(jsonSettings),
contentType: 'application/json',
type: 'POST'
}).done(function(data) {
if (data.status == "success") {
waitForDomainServerRestart(function() {
dialog.modal('hide');
if (onSuccessfullyAdded) {
onSuccessfullyAdded(places_by_id[placeID].name, domainID);
}
});
} else {
bootbox.alert("Failed to add place");
}
}).fail(function() {
bootbox.alert("Failed to add place");
});
}
function finishSettingUpPlace(domainID) {
sendUpdatePlaceRequest(
placeID,
placePath,
domainID,
false,
function(data) {
$(Settings.DOMAIN_ID_SELECTOR).val(domainID).change();
dialog.modal('hide')
if (domainID) {
finalizeSaveDomainID(domainID);
} else {
if (onSuccessfullyAdded) {
onSuccessfullyAdded(places_by_id[placeID].name);
}
}
},
function(data) {
$('.add-place-confirm-button').removeAttr('disabled');
$('.add-place-confirm-button').html(Strings.ADD_PLACE_CONFIRM_BUTTON);
$('.add-place-cancel-button').removeAttr('disabled');
bootbox.alert(Strings.ADD_PLACE_UNKNOWN_ERROR);
}
);
}
if (currentDomainIDType === DOMAIN_ID_TYPE_FULL) {
finishSettingUpPlace();
} else {
sendCreateDomainRequest(function(domainID) {
console.log("Created domain", domainID);
finishSettingUpPlace(domainID);
}, function() {
$('.add-place-confirm-button').removeAttr('disabled');
$('.add-place-confirm-button').html(Strings.ADD_PLACE_CONFIRM_BUTTON);
$('.add-place-cancel-button').removeAttr('disabled');
bootbox.alert(Strings.ADD_PLACE_UNKNOWN_ERROR);
bootbox.alert("FAIL");
});
}
return false;
}
}
} else {
modal_buttons["success"] = {
label: Strings.ADD_PLACE_NO_PLACES_BUTTON,
callback: function() {
window.open(URLs.METAVERSE_URL + "/user/places", '_blank');
}
}
modal_body = Strings.ADD_PLACE_NO_PLACES_MESSAGE;
}
dialog = bootbox.dialog({
title: Strings.ADD_PLACE_TITLE,
message: modal_body,
closeButton: false,
buttons: modal_buttons
});
} else {
bootbox.alert(Strings.ADD_PLACE_UNABLE_TO_LOAD_ERROR);
}
},
error: function() {
bootbox.alert(Strings.ADD_PLACE_UNABLE_TO_LOAD_ERROR);
},
complete: function() {
loadingDialog.modal('hide');
}
});
} else {
bootbox.alert({
title: Strings.ADD_PLACE_NOT_CONNECTED_TITLE,
message: Strings.ADD_PLACE_NOT_CONNECTED_MESSAGE
})
}
}
function sendCreateDomainRequest(onSuccess, onError) {
$.ajax({
url: '/api/domains',
dataType: 'json',
type: 'POST',
data: { label: "" },
success: function(data) {
onSuccess(data.domain.id);
},
error: onError
});
}
function waitForDomainServerRestart(callback) {
function checkForDomainUp() {
$.ajax('', {
success: function() {
callback();
},
error: function() {
setTimeout(checkForDomainUp, 50);
}
});
}
setTimeout(checkForDomainUp, 10);
}
function prepareAccessTokenPrompt(callback) {
swal({
title: "Connect Account",
type: "input",
text: "Paste your created access token here." +
"</br></br>If you did not successfully create an access token click cancel below and attempt to connect your account again.</br></br>",
showCancelButton: true,
closeOnConfirm: false,
html: true
}, function(inputValue){
if (inputValue === false) {
return false;
}
if (inputValue === "") {
swal.showInputError("Please paste your access token in the input field.")
return false
}
if (callback) {
callback(inputValue);
}
swal.close();
});
}

View file

@ -9,16 +9,18 @@
<div class="row">
<div class="col-md-3 col-sm-3" id="setup-sidebar-col">
<div id="setup-sidebar" class="hidden-xs" data-spy="affix" data-offset-top="55" data-clampedwidth="#setup-sidebar-col">
<div id="setup-sidebar" data-clampedwidth="#setup-sidebar-col">
<script id="list-group-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<% panelID = group.name ? group.name : group.html_id %>
<li>
<a href="#<%- panelID %>" class="list-group-item">
<span class="badge"></span>
<%- group.label %>
</a>
</li>
<% if (!group.hidden) { %>
<% panelID = group.name ? group.name : group.html_id %>
<li>
<a href="#<%- panelID %>" class="list-group-item">
<span class="badge"></span>
<%- group.label %>
</a>
</li>
<% } %>
<% }); %>
</script>
@ -26,49 +28,63 @@
</ul>
<button id="advanced-toggle-button" class="btn btn-info advanced-toggle">Show advanced</button>
<button class="btn btn-success save-button">Save</button>
<button class="btn btn-success save-button" disabled>Save</button>
<div id="manage-cloud-domains-link" style="display: none;">
<a href="https://highfidelity.com/user/cloud_domains" target="_blank" class="blue-link">Manage Cloud Hosted Domains</a>
</div>
</div>
</div>
<div class="col-md-9 col-sm-9 col-xs-12">
<div id="xs-advanced-container" class="col-xs-12 hidden-sm hidden-md hidden-lg">
<button id="advanced-toggle-button-xs" class="btn btn-info advanced-toggle">Show advanced</button>
</div>
<div class="col-xs-12">
<div id="cloud-domains-alert" class="alert alert-info alert-dismissible" role="alert" style="display: none;">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span class="alert-link">
<a href="https://highfidelity.com/user/cloud_domains" target="_blank" class="blue-link">Visit Cloud Hosted Domains</a> to manage all your cloud domains
</span>
</div>
<form id="settings-form" role="form">
<script id="panels-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<% var settings = _.partition(group.settings, function(value, index) { return !value.deprecated })[0] %>
<% split_settings = _.partition(settings, function(value, index) { return !value.advanced }) %>
<% isAdvanced = _.isEmpty(split_settings[0]) %>
<% if (isAdvanced) { %>
<% $("a[href=#" + group.name + "]").addClass('advanced-setting').hide() %>
<% } %>
<% if (!group.hidden) { %>
<% var settings = _.partition(group.settings, function(value, index) { return !value.deprecated })[0] %>
<% split_settings = _.partition(settings, function(value, index) { return !value.advanced }) %>
<% isAdvanced = _.isEmpty(split_settings[0]) && !_.isEmpty(split_settings[1]) %>
<% if (isAdvanced) { %>
<% $("a[href=#" + group.name + "]").addClass('advanced-setting').hide() %>
<% } %>
<% isGrouped = !!group.name %>
<% panelID = isGrouped ? group.name : group.html_id %>
<% isGrouped = !!group.name %>
<% panelID = isGrouped ? group.name : group.html_id %>
<div class="panel panel-default<%- (isAdvanced) ? ' advanced-setting' : '' %><%- (isGrouped) ? ' grouped' : '' %>"
id="<%- panelID %>">
<div class="panel-heading">
<h3 class="panel-title"><%- group.label %></h3>
</div>
<div class="panel-body">
<% _.each(split_settings[0], function(setting) { %>
<% keypath = isGrouped ? group.name + "." + setting.name : setting.name %>
<%= getFormGroup(keypath, setting, values, false) %>
<% }); %>
<% if (!_.isEmpty(split_settings[1])) { %>
<% $("#advanced-toggle-button").show() %>
<% _.each(split_settings[1], function(setting) { %>
<div class="panel panel-default<%- (isAdvanced) ? ' advanced-setting' : '' %><%- (isGrouped) ? ' grouped' : '' %>"
id="<%- panelID %>">
<div class="panel-heading">
<h3 class="panel-title"><%- group.label %></h3>
</div>
<div class="panel-body">
<% _.each(split_settings[0], function(setting) { %>
<% keypath = isGrouped ? group.name + "." + setting.name : setting.name %>
<%= getFormGroup(keypath, setting, values, true) %>
<%= getFormGroup(keypath, setting, values, false) %>
<% }); %>
<% }%>
<% if (!_.isEmpty(split_settings[1])) { %>
<% $("#advanced-toggle-button").show() %>
<% _.each(split_settings[1], function(setting) { %>
<% keypath = isGrouped ? group.name + "." + setting.name : setting.name %>
<%= getFormGroup(keypath, setting, values, true) %>
<% }); %>
<% }%>
</div>
</div>
</div>
<% } %>
<% }); %>
</script>
<div id="panels"></div>
@ -85,8 +101,9 @@
<script src='/js/underscore-min.js'></script>
<script src='/js/underscore-keypath.min.js'></script>
<script src='/js/bootbox.min.js'></script>
<script src='js/bootstrap-switch.min.js'></script>
<script src='js/settings.js'></script>
<script src='/js/sha256.js'></script>
<script src='js/form2js.min.js'></script>
<script src='js/sha256.js'></script>
<script src='js/bootstrap-switch.min.js'></script>
<script src='/js/shared.js'></script>
<script src='js/settings.js'></script>
<!--#include virtual="page-end.html"-->

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,89 @@
label {
font-style: normal;
font-weight: normal;
line-height: 24px;
font-size: 16px;
color: #373A3C;
}
.step-title {
margin-bottom: 20px;
line-height: 26px;
font-size: 24px;
color: #373A3C;
}
.step-description {
line-height: 24px;
font-size: 16px;
color: #818A91;
}
.step-info {
font-style: normal;
font-weight: normal;
line-height: 24px;
font-size: 16px;
color: #373A3C;
}
#admin-row {
margin-top: 20px;
margin-bottom: 20px;
}
#connect-question {
margin-top: 10px;
}
#connect-account-btn {
margin-top: 30px;
margin-bottom: 205px;
}
#place-name-group {
margin-top: 42px;
margin-bottom: 140px;
display: inline-block;
}
#place-name-link {
line-height: 38px;
font-size: 32px;
display: inline-block;
color: #373A3C;
}
#place-name-edit {
text-align: right;
}
#change-place-name {
line-height: 24px;
font-size: 18px;
}
#rez-options-row {
margin-bottom: 60px;
}
#verify-password-row {
margin-bottom: 33px;
}
#checkmark-image {
margin-top: 66px;
margin-bottom: 59px;
width: 169px;
height: 169px;
}
#visit-domain-row {
margin-bottom: 68px;
}

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>domain-server</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="/css/style.css" rel="stylesheet" media="screen">
<link href="/wizard/css/style.css" rel="stylesheet" media="screen">
<link href="/css/sweetalert.css" rel="stylesheet" media="screen">
<link href="/css/bootstrap-switch.min.css" rel="stylesheet" media="screen">
<script src='/js/sweetalert.min.js'></script>
</head>
<body>
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="container-fluid">
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<a class="navbar-brand step-info" href="#">Setup Wizard (Domain Server Settings)</a>
<div class="navbar-form navbar-right">
<button id="skip-wizard-button" type="button" class="btn btn-default" style="display:none;">Skip Wizard</button>
</div>
</div>
</div><!-- /.container-fluid -->
</nav>
<div class="container">

View file

@ -0,0 +1,229 @@
<!--#include virtual="wizard/header.html"-->
<div class="wizard-step desktop-only col-md-6 col-centered" style="display: none;">
<h4 class="step-title"></h4>
<dl class="row">
<dd class="col-md-12">
<span class='step-description'>By connecting your High Fidelity Account you will be granting access to your account information.</span>
</dd>
</dl>
<dl class="row">
<dd class="col-md-12">
<a id="connect-account-btn" role="button" class="btn btn-primary btn-md btn-block" target="_blank">Connect your High Fidelity account</a>
</dd>
</dl>
<dl class="row">
<dd class="col-md-3">
<button type="button" class="btn btn-md btn-block btn-default next-button">Skip</button>
</dd>
</dl>
</div>
<div class="wizard-step col-md-8 col-centered" style="display: none;">
<h4 class="step-title"></h4>
<div class="row">
<div class="col-md-12">
<span class='step-description'>
<a target='_blank' href='https://docs.highfidelity.com/create-and-explore/start-working-in-your-sandbox/place-names'>Place names</a> are similar to web addresses. Users who want to visit your domain can
enter its Place Name in High Fidelity's Interface. You can choose a Place Name for your domain.</br>
People can also use your <b>domain's IP address (shown below)</b> to visit your High Fidelity domain.
</span>
</div>
</div>
<div class="row">
<div class="centered-hack-parent">
<div id="place-name-group" class="centered-hack">
<p id="place-name-link"></p>
<div id="place-name-edit">
<span class='glyphicon glyphicon-pencil'></span>
<a href="#" id="change-place-name">Choose a custom Place Name instead</a>
</div>
</div>
</div>
</div>
<dl class="row">
<dd class="col-md-3">
<button type="button" class="btn btn-md btn-block btn-default back-button">Back</button>
</dd>
<dd class="col-md-3 col-md-offset-6">
<button type="button" class="btn btn-md btn-block btn-primary next-button">Next</button>
</dd>
</dl>
</div>
<div class="wizard-step col-md-9 col-centered" style="display: none;">
<h4 class="step-title"></h4>
<div class="row">
<p id="permissions-description" class="col-md-12 step-info"><b>Localhost</b> has been granted administrator privileges to this domain. (Localhost is any</br>user on the same machine as the High Fidelity Server)</p>
</div>
<div id="admin-row" class="row">
<p class="col-md-6">
<span id="admin-description" class="step-info"><b>Add your High Fidelity username</b> and any other usernames to grant administrator privileges.</span>
<span class='glyphicon glyphicon-info-sign' data-toggle="tooltip" title="Users who will have all the permissions for this domain."></span>
</p>
<div class="col-md-6">
<input id="admin-usernames" type="text" class="form-control">
<span class="help-block text-right">separate by commas (user1, user2,..)</span>
</div>
</div>
<div class="row">
<div class="col-md-12">
<span class="step-description">Grant basic permissions to other users. You can change these later.</span>
</div>
</div>
<div class="row">
<div class="col-md-12">
<p id="connect-question" class="step-info">
Who can connect to your domain?
<span class='glyphicon glyphicon-info-sign' data-toggle='tooltip' title='You can set this to allow a user to connect to this domain.'></span>
</p>
</div>
</div>
<div class="row">
<p class="col-md-2">
<label>
<input id="connect-none" name="connect-radio" type="radio" value="none" checked> None
<span class='glyphicon glyphicon-info-sign' data-toggle='tooltip' title='Only the admins of this domain'></span>
</label>
</p>
<p class="col-md-3">
<label>
<input id="connect-friends" name="connect-radio" type="radio" value="friends"> Friends
<span class='glyphicon glyphicon-info-sign' data-toggle='tooltip' title='Users who are your Friends in High Fidelity'></span>
</label>
</p>
<p class="col-md-5">
<label>
<input id="connect-logged-in" name="connect-radio" type="radio" value="logged-in"> Users logged in to High Fidelity
<span class='glyphicon glyphicon-info-sign' data-toggle='tooltip' title='Users who are currently logged into High Fidelity'></span>
</label>
</p>
<p class="col-md-2">
<label>
<input id="connect-everyone" name="connect-radio" type="radio" value="everyone"> Everyone
<span class='glyphicon glyphicon-info-sign' data-toggle='tooltip' title="Users who aren't logged into High Fidelity"></span>
</label>
</p>
</div>
<div class="row">
<div class="col-md-12">
<p class="step-info">
Who can rez items in your domain?
<span class='glyphicon glyphicon-info-sign' data-toggle='tooltip' title='You can set this to allow a user to create entities in this domain.'></span>
</p>
</div>
</div>
<div id="rez-options-row" class="row">
<p class="col-md-2">
<label>
<input id="rez-none" name="rez-radio" type="radio" value="none" checked> None
<span class='glyphicon glyphicon-info-sign' data-toggle='tooltip' title='Only the admins of this domain'></span>
</label>
</p>
<p class="col-md-3">
<label>
<input id="rez-friends" name="rez-radio" type="radio" value="friends"> Friends
<span class='glyphicon glyphicon-info-sign' data-toggle='tooltip' title='Users who are your Friends in High Fidelity'></span>
</label>
</p>
<p class="col-md-5">
<label>
<input id="rez-logged-in" name="rez-radio" type="radio" value="logged-in"> Users logged in to High Fidelity
<span class='glyphicon glyphicon-info-sign' data-toggle='tooltip' title='Users who are currently logged into High Fidelity'></span>
</label>
</p>
<p class="col-md-2">
<label>
<input id="rez-everyone" name="rez-radio" type="radio" value="everyone"> Everyone
<span class='glyphicon glyphicon-info-sign' data-toggle='tooltip' title="Users who aren't logged into High Fidelity"></span>
</label>
</p>
</div>
<div class="row">
<div class="col-md-3 col-md-offset-9">
<button id="save-permissions" type="button" class="btn btn-md btn-block btn-primary">Next</button>
</div>
</div>
</div>
<div class="wizard-step cloud-only col-md-7 col-centered" style="display: none;">
<h4 class="step-title"></h4>
<dl class="row">
<dd class="col-md-12">
<span class='step-description'>
Your server settings are currently accessible without a username and password.
Adding credentials will ensure that only authorized users have access to modify the settings.
</span>
</dd>
</dl>
<div class="row">
<p class="col-md-12 step-info">Create a username and password to secure the access to your domain server settings.</p>
</div>
<dl class="row">
<dt class="col-md-4 step-info">Username</dt>
<dd class="col-md-8">
<input id="http_username" type="text" class="form-control">
<span class='help-block'>This does not have to be your High Fidelity username</span>
</dd>
</dl>
<dl class="row">
<dt class="col-md-4 step-info">Enter password</dt>
<dd class="col-md-8">
<input id="http_password" type="password" class="form-control">
<span class='help-block'>This should not be the same as your High Fidelity password</span>
</dd>
</dl>
<dl id="verify-password-row" class="row">
<dt class="col-md-4 step-info">Re-enter password</dt>
<dd class="col-md-8">
<input id="verify_http_password" type="password" class="form-control">
</dd>
</dl>
<dl class="row">
<dd class="col-md-3 col-md-offset-9">
<button id="save-username-password" type="button" class="btn btn-md btn-block btn-primary">Finish</button>
</dd>
</dl>
</div>
<div class="wizard-step cloud-only col-md-7 col-centered" style="display: none;">
<div class="row">
<div class="col-xs-4 col-centered">
<img id="checkmark-image" src="../images/checkmark.svg">
</div>
</div>
<div class="row">
<div class="col-md-12">
<p class="step-info">Congratulations! You have successfully setup and configured your cloud hosted domain.</p>
</div>
</div>
<div id="visit-domain-row" class="row">
<div class="col-md-12">
<label><input id="go-to-domain" class="form-check-input" type="checkbox"> Visit domain in VR now</label>
</div>
</div>
<dl class="row">
<dd class="col-md-5 col-md-offset-7">
<button id="explore-settings" type="button" class="btn btn-md btn-block btn-primary">Explore all domain server settings</button>
</dd>
</dl>
</div>
<!--#include virtual="footer.html"-->
<script src='/js/underscore-min.js'></script>
<script src='/js/bootbox.min.js'></script>
<script src='/js/sha256.js'></script>
<script src='/js/shared.js'></script>
<script src='js/wizard.js'></script>
<!--#include virtual="page-end.html"-->

View file

@ -0,0 +1,471 @@
var Metaverse = {
accessToken: null
}
$(document).ready(function(){
Strings.ADD_PLACE_NOT_CONNECTED_MESSAGE = "You must have an access token to query your High Fidelity places.<br><br>" +
"Please go back and connect your account.";
$('#connect-account-btn').attr('href', URLs.METAVERSE_URL + "/user/tokens/new?for_domain_server=true");
$('[data-toggle="tooltip"]').tooltip();
$('body').on('click', '.next-button', function() {
goToNextStep();
});
$('body').on('click', '.back-button', function() {
goToPreviousStep();
});
$('body').on('click', '#skip-wizard-button', function() {
skipWizard();
})
$('body').on('click', '#connect-account-btn', function() {
$(this).blur();
prepareAccessTokenPrompt(function(accessToken) {
Metaverse.accessToken = accessToken;
saveAccessToken();
});
});
$('body').on('click', '#save-permissions', function() {
savePermissions();
});
function triggerSaveUsernamePassword(event) {
if (event.keyCode === 13) {
$("#save-username-password").click();
}
}
$("#http_username").keyup(triggerSaveUsernamePassword);
$("#http_password").keyup(triggerSaveUsernamePassword);
$("#verify_http_password").keyup(triggerSaveUsernamePassword);
$('body').on('click', '#save-username-password', function() {
saveUsernamePassword();
});
$('body').on('click', '#change-place-name', function() {
chooseFromHighFidelityPlaces(Settings.data.values.metaverse.access_token, "/0,-10,0", function(placeName) {
updatePlaceNameLink(placeName);
});
});
$('body').on('click', '#explore-settings', function() {
exploreSettings();
});
reloadSettings(function(success) {
if (success) {
setupWizardSteps();
updatePlaceNameDisplay();
updateUsernameDisplay();
} else {
swal({
title: '',
type: 'error',
text: "There was a problem loading the domain settings.\nPlease refresh the page to try again.",
});
}
});
});
function setupWizardSteps() {
var stepsCompleted = Settings.data.values.wizard.steps_completed;
var steps = null;
if (Settings.data.values.wizard.cloud_domain) {
$('.desktop-only').remove();
$('.wizard-step').find('.back-button').hide();
steps = $('.wizard-step');
$(steps).each(function(i) {
$(this).children(".step-title").text("Step " + (i + 1) + " of " + (steps.length - 1));
});
$('#permissions-description').html('You <span id="username-display"></span>have been assigned administrator privileges to this domain.');
$('#admin-description').html('Add more High Fidelity usernames to grant administrator privileges.');
} else {
$('.cloud-only').remove();
$('#save-permissions').text("Finish");
steps = $('.wizard-step');
$(steps).each(function(i) {
$(this).children(".step-title").text("Step " + (i + 1) + " of " + steps.length);
});
if (stepsCompleted == 0) {
$('#skip-wizard-button').show();
}
}
var currentStep = steps[stepsCompleted];
$(currentStep).show();
}
function updatePlaceNameLink(address) {
if (address) {
$('#place-name-link').html('Your domain is reachable at: <a target="_blank" href="' + URLs.PLACE_URL + '/' + address + '">' + address + '</a>');
}
}
function updatePlaceNameDisplay() {
if (Settings.data.values.metaverse.id) {
$.getJSON(URLs.METAVERSE_URL + '/api/v1/domains/' + Settings.data.values.metaverse.id, function(data) {
if (data.status === 'success') {
if (data.domain.default_place_name) {
// Place name
updatePlaceNameLink(data.domain.default_place_name);
} else if (data.domain.name) {
// Temporary name
updatePlaceNameLink(data.domain.name);
} else if (data.domain.network_address) {
if (data.domain.network_port !== 40102) {
// IP:PORT
updatePlaceNameLink(data.domain.network_address + ':' + data.domain.network_port);
} else {
// IP
updatePlaceNameLink(data.domain.network_address);
}
}
} else {
console.warn('Request Failed');
}
}).fail(function() {
console.warn('Request Failed');
});
} else {
console.warn('No metaverse domain ID!');
}
}
function updateUsernameDisplay() {
var permissions = Settings.data.values.security.permissions;
if (permissions.length > 0) {
$('#username-display').html('<b>(' + permissions[0].permissions_id + ')</b> ');
}
}
function reloadSettings(callback) {
$.getJSON('/settings.json', function(data){
Settings.data = data;
if (callback) {
// call the callback now that settings are loaded
callback(true);
}
}).fail(function() {
if (callback) {
// call the failure object since settings load failed
callback(false)
}
});
}
function postSettings(jsonSettings, callback) {
console.log("----- SAVING ------");
console.log(JSON.stringify(jsonSettings));
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
$.ajax('/settings.json', {
data: JSON.stringify(jsonSettings),
contentType: 'application/json',
type: 'POST'
}).done(function(data){
if (data.status == "success") {
if (callback) {
callback();
}
reloadSettings();
} else {
swal("Error", Strings.LOADING_SETTINGS_ERROR)
reloadSettings();
}
}).fail(function(){
swal("Error", Strings.LOADING_SETTINGS_ERROR)
reloadSettings();
});
}
function goToNextStep() {
$('#skip-wizard-button').hide();
var currentStep = $('body').find('.wizard-step:visible');
var nextStep = currentStep.next('.wizard-step');
var formJSON = {
"wizard": {}
}
if (nextStep.length > 0) {
currentStep.hide();
nextStep.show();
var currentStepNumber = parseInt(Settings.data.values.wizard.steps_completed) + 1;
postSettings({
"wizard": {
"steps_completed": currentStepNumber.toString()
}
});
} else {
postSettings({
"wizard": {
"steps_completed": "0",
"completed_once": true
}
}, redirectToSettings);
}
}
function goToPreviousStep() {
var currentStep = $('body').find('.wizard-step:visible');
var previousStep = currentStep.prev('.wizard-step');
var formJSON = {
"wizard": {}
}
if (previousStep.length > 0) {
currentStep.hide();
previousStep.show();
var currentStepNumber = parseInt(Settings.data.values.wizard.steps_completed) - 1;
postSettings({
"wizard": {
"steps_completed": currentStepNumber.toString()
}
});
}
}
function skipWizard() {
postSettings({
"wizard": {
"steps_completed": "0",
"completed_once": true
}
}, redirectToSettings);
}
function redirectToSettings() {
var redirectURL = "/settings" + location.search;
if (Settings.data.values.wizard.cloud_domain) {
if (location.search.length > 0) {
redirectURL += "&";
} else {
redirectURL += "?";
}
redirectURL += "cloud-wizard-exit";
}
location.href = redirectURL;
}
function saveAccessToken() {
var formJSON = {
"metaverse": {}
}
if (Metaverse.accessToken) {
formJSON.metaverse.access_token = Metaverse.accessToken;
}
// remove focus from the button
$(this).blur();
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
postSettings(formJSON, goToNextStep);
}
function getSettingDescriptionForKey(groupKey, settingKey) {
for (var i in Settings.data.descriptions) {
var group = Settings.data.descriptions[i];
if (group.name === groupKey) {
for (var j in group.settings) {
var setting = group.settings[j];
if (setting.name === settingKey) {
return setting;
}
}
}
}
}
function savePermissions() {
var localhostPermissions = (Settings.data.values.wizard.cloud_domain !== true)
var anonymousCanConnect = false;
var friendsCanConnect = false;
var loggedInCanConnect = false;
var anonymousCanRez = false;
var friendsCanRez = false;
var loggedInCanRez = false;
var connectValue = $('input[name=connect-radio]:checked').val();
var rezValue = $('input[name=rez-radio]:checked').val();
switch (connectValue) {
case "friends":
friendsCanConnect = true;
break;
case "logged-in":
friendsCanConnect = true;
loggedInCanConnect = true;
break;
case "everyone":
anonymousCanConnect = true;
friendsCanConnect = true;
loggedInCanConnect = true;
break;
}
switch (rezValue) {
case "friends":
friendsCanRez = true;
break;
case "logged-in":
friendsCanRez = true;
loggedInCanRez = true;
break;
case "everyone":
anonymousCanRez = true;
friendsCanRez = true;
loggedInCanRez = true;
break;
}
var admins = $('#admin-usernames').val().split(',');
var existingAdmins = Settings.data.values.security.permissions.map(function(value) {
return value.permissions_id;
});
admins = admins.concat(existingAdmins);
// Filter out unique values
admins = _.uniq(admins.map(function(username) {
return username.trim();
})).filter(function(username) {
return username !== "";
});
var formJSON = {
"security": {
"standard_permissions": [
{
"id_can_connect": anonymousCanConnect,
"id_can_rez": anonymousCanRez,
"id_can_rez_certified": anonymousCanRez,
"id_can_rez_tmp": anonymousCanRez,
"id_can_rez_tmp_certified": anonymousCanRez,
"permissions_id": "anonymous"
},
{
"id_can_connect": friendsCanConnect,
"id_can_rez": friendsCanRez,
"id_can_rez_certified": friendsCanRez,
"id_can_rez_tmp": friendsCanRez,
"id_can_rez_tmp_certified": friendsCanRez,
"permissions_id": "friends"
},
{
"id_can_connect": loggedInCanConnect,
"id_can_rez": loggedInCanRez,
"id_can_rez_certified": loggedInCanRez,
"id_can_rez_tmp": loggedInCanRez,
"id_can_rez_tmp_certified": loggedInCanRez,
"permissions_id": "logged-in"
},
{
"id_can_adjust_locks": localhostPermissions,
"id_can_connect": localhostPermissions,
"id_can_connect_past_max_capacity": localhostPermissions,
"id_can_kick": localhostPermissions,
"id_can_replace_content": localhostPermissions,
"id_can_rez": localhostPermissions,
"id_can_rez_certified": localhostPermissions,
"id_can_rez_tmp": localhostPermissions,
"id_can_rez_tmp_certified": localhostPermissions,
"id_can_write_to_asset_server": localhostPermissions,
"permissions_id": "localhost"
}
]
}
}
if (admins.length > 0) {
formJSON.security.permissions = [];
var permissionsDesc = getSettingDescriptionForKey("security", "permissions");
for (var i in admins) {
var admin = admins[i];
var perms = {};
for (var i in permissionsDesc.columns) {
var name = permissionsDesc.columns[i].name;
if (name === "permissions_id") {
perms[name] = admin;
} else {
perms[name] = true;
}
}
formJSON.security.permissions.push(perms);
}
}
// remove focus from the button
$(this).blur();
postSettings(formJSON, goToNextStep);
}
function saveUsernamePassword() {
var username = $("#http_username").val();
var password = $("#http_password").val();
var verify_password = $("#verify_http_password").val();
if (username.length == 0) {
bootbox.alert({ "message": "You must set a username!", "title": "Username Error" });
return;
}
if (password.length == 0) {
bootbox.alert({ "message": "You must set a password!", "title": "Password Error" });
return;
}
if (password != verify_password) {
bootbox.alert({ "message": "Passwords must match!", "title": "Password Error" });
return;
}
var currentStepNumber = parseInt(Settings.data.values.wizard.steps_completed) + 1;
var formJSON = {
"security": {
"http_username": username,
"http_password": sha256_digest(password)
},
"wizard": {
"steps_completed": currentStepNumber.toString()
}
}
// remove focus from the button
$(this).blur();
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
postSettings(formJSON, function() {
location.reload();
});
}
function exploreSettings() {
if ($('#go-to-domain').is(":checked")) {
var link = $('#place-name-link a:first');
if (link.length > 0) {
window.open(link.attr("href"));
}
}
goToNextStep();
}

View file

@ -46,6 +46,8 @@
#include "DomainServerNodeData.h"
#include "NodeConnectionData.h"
const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token";
int const DomainServer::EXIT_CODE_REBOOT = 234923;
#if USE_STABLE_GLOBAL_SERVICES
@ -54,6 +56,82 @@ const QString ICE_SERVER_DEFAULT_HOSTNAME = "ice.highfidelity.com";
const QString ICE_SERVER_DEFAULT_HOSTNAME = "dev-ice.highfidelity.com";
#endif
bool DomainServer::forwardMetaverseAPIRequest(HTTPConnection* connection,
const QString& metaversePath,
const QString& requestSubobjectKey,
std::initializer_list<QString> requiredData,
std::initializer_list<QString> optionalData,
bool requireAccessToken) {
auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH);
if (accessTokenVariant == nullptr && requireAccessToken) {
connection->respond(HTTPConnection::StatusCode400, "User access token has not been set");
return true;
}
QJsonObject subobject;
auto params = connection->parseUrlEncodedForm();
for (auto& key : requiredData) {
auto it = params.find(key);
if (it == params.end()) {
auto error = "Bad request, expected param '" + key + "'";
connection->respond(HTTPConnection::StatusCode400, error.toLatin1());
return true;
}
subobject.insert(key, it.value());
}
for (auto& key : optionalData) {
auto it = params.find(key);
if (it != params.end()) {
subobject.insert(key, it.value());
}
}
QJsonObject root;
root.insert(requestSubobjectKey, subobject);
QJsonDocument doc { root };
QUrl url { NetworkingConstants::METAVERSE_SERVER_URL.toString() + metaversePath };
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (accessTokenVariant != nullptr) {
auto accessTokenHeader = QString("Bearer ") + accessTokenVariant->toString();
req.setRawHeader("Authorization", accessTokenHeader.toLatin1());
}
QNetworkReply* reply;
auto method = connection->requestOperation();
if (method == QNetworkAccessManager::GetOperation) {
reply = NetworkAccessManager::getInstance().get(req);
} else if (method == QNetworkAccessManager::PostOperation) {
reply = NetworkAccessManager::getInstance().post(req, doc.toJson());
} else if (method == QNetworkAccessManager::PutOperation) {
reply = NetworkAccessManager::getInstance().put(req, doc.toJson());
} else {
connection->respond(HTTPConnection::StatusCode400, "Error forwarding request, unsupported method");
return true;
}
connect(reply, &QNetworkReply::finished, this, [reply, connection]() {
if (reply->error() != QNetworkReply::NoError) {
auto data = reply->readAll();
qDebug() << "Got error response from metaverse server (" << reply->url() << "): " << data << reply->errorString();
connection->respond(HTTPConnection::StatusCode400, data);
return;
}
connection->respond(HTTPConnection::StatusCode200, reply->readAll());
});
return true;
}
DomainServer::DomainServer(int argc, char* argv[]) :
QCoreApplication(argc, argv),
_gatekeeper(this),
@ -614,8 +692,6 @@ void DomainServer::setupNodeListAndAssignments() {
nodeList->setPacketFilterOperator(&DomainServer::isPacketVerified);
}
const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token";
bool DomainServer::resetAccountManagerAccessToken() {
if (!_oauthProviderURL.isEmpty()) {
// check for an access-token in our settings, can optionally be overidden by env value
@ -1731,6 +1807,8 @@ QString DomainServer::pathForRedirect(QString path) const {
return "http://" + _hostname + ":" + QString::number(_httpManager.serverPort()) + path;
}
const QString URI_OAUTH = "/oauth";
bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) {
const QString JSON_MIME_TYPE = "application/json";
@ -1740,15 +1818,23 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
const QString URI_SETTINGS = "/settings";
const QString URI_ENTITY_FILE_UPLOAD = "/content/upload";
const QString URI_RESTART = "/restart";
const QString URI_API_PLACES = "/api/places";
const QString URI_API_DOMAINS = "/api/domains";
const QString URI_API_DOMAINS_ID = "/api/domains/";
const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
auto nodeList = DependencyManager::get<LimitedNodeList>();
// allow sub-handlers to handle requests that do not require authentication
if (_settingsManager.handlePublicHTTPRequest(connection, url)) {
auto getSetting = [this](QString keyPath, QVariant& value) -> bool {
QVariantMap& settingsMap = _settingsManager.getSettingsMap();
QVariant* var = valueForKeyPath(settingsMap, keyPath);
if (var == nullptr) {
return false;
}
value = *var;
return true;
}
};
// check if this is a request for a scripted assignment (with a temp unique UUID)
const QString ASSIGNMENT_REGEX_STRING = QString("\\%1\\/(%2)\\/?$").arg(URI_ASSIGNMENT).arg(UUID_REGEX_STRING);
@ -1810,6 +1896,31 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
return true;
}
// Check if we should redirect/prevent access to the wizard
if (connection->requestOperation() == QNetworkAccessManager::GetOperation) {
const QString URI_WIZARD = "/wizard/";
const QString WIZARD_COMPLETED_ONCE_KEY_PATH = "wizard.completed_once";
const QVariant* wizardCompletedOnce = valueForKeyPath(_settingsManager.getSettingsMap(), WIZARD_COMPLETED_ONCE_KEY_PATH);
const bool completedOnce = wizardCompletedOnce && wizardCompletedOnce->toBool();
if (url.path() != URI_WIZARD && url.path().endsWith('/') && !completedOnce) {
// First visit, redirect to the wizard
QUrl redirectedURL = url;
redirectedURL.setPath(URI_WIZARD);
Headers redirectHeaders;
redirectHeaders.insert("Location", redirectedURL.toEncoded());
connection->respond(HTTPConnection::StatusCode302,
QByteArray(), HTTPConnection::DefaultContentType, redirectHeaders);
return true;
} else if (url.path() == URI_WIZARD && completedOnce) {
// Wizard already completed, return 404
connection->respond(HTTPConnection::StatusCode404, "Resource not found.");
return true;
}
}
if (connection->requestOperation() == QNetworkAccessManager::GetOperation) {
if (url.path() == "/assignments.json") {
// user is asking for json list of assignments
@ -1899,6 +2010,13 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
connection->respond(HTTPConnection::StatusCode200);
restart();
return true;
} else if (url.path() == URI_API_DOMAINS) {
return forwardMetaverseAPIRequest(connection, "/api/v1/domains", "");
} else if (url.path().startsWith(URI_API_DOMAINS_ID)) {
auto id = url.path().mid(URI_API_DOMAINS_ID.length());
return forwardMetaverseAPIRequest(connection, "/api/v1/domains/" + id, "", {}, {}, false);
} else if (url.path() == URI_API_PLACES) {
return forwardMetaverseAPIRequest(connection, "/api/v1/user/places", "");
} else {
// check if this is for json stats for a node
const QString NODE_JSON_REGEX_STRING = QString("\\%1\\/(%2).json\\/?$").arg(URI_NODES).arg(UUID_REGEX_STRING);
@ -1978,8 +2096,6 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
// this is an entity file upload, ask the HTTPConnection to parse the data
QList<FormData> formData = connection->parseFormData();
Headers redirectHeaders;
if (formData.size() > 0 && formData[0].second.size() > 0) {
// invoke our method to hand the new octree file off to the octree server
QMetaObject::invokeMethod(this, "handleOctreeFileReplacement",
@ -1993,7 +2109,98 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
}
return true;
} else if (url.path() == "/domain_settings") {
auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH);
if (!accessTokenVariant) {
connection->respond(HTTPConnection::StatusCode400);
return true;
}
} else if (url.path() == URI_API_DOMAINS) {
return forwardMetaverseAPIRequest(connection, "/api/v1/domains", "domain", { "label" });
}
} else if (connection->requestOperation() == QNetworkAccessManager::PutOperation) {
if (url.path() == URI_API_DOMAINS) {
QVariant domainSetting;
if (!getSetting(METAVERSE_DOMAIN_ID_KEY_PATH, domainSetting)) {
connection->respond(HTTPConnection::StatusCode400, "Domain id has not been set");
return true;
}
auto domainID = domainSetting.toString();
return forwardMetaverseAPIRequest(connection, "/api/v1/domains/" + domainID, "domain",
{ }, { "network_address", "network_port", "label" });
} else if (url.path() == URI_API_PLACES) {
auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH);
if (!accessTokenVariant->isValid()) {
connection->respond(HTTPConnection::StatusCode400, "User access token has not been set");
return true;
}
auto params = connection->parseUrlEncodedForm();
auto it = params.find("place_id");
if (it == params.end()) {
connection->respond(HTTPConnection::StatusCode400);
return true;
}
QString place_id = it.value();
it = params.find("path");
if (it == params.end()) {
connection->respond(HTTPConnection::StatusCode400);
return true;
}
QString path = it.value();
it = params.find("domain_id");
QString domainID;
if (it == params.end()) {
QVariant domainSetting;
if (!getSetting(METAVERSE_DOMAIN_ID_KEY_PATH, domainSetting)) {
connection->respond(HTTPConnection::StatusCode400);
return true;
}
domainID = domainSetting.toString();
} else {
domainID = it.value();
}
QJsonObject root {
{
"place",
QJsonObject({
{ "pointee_query", domainID },
{ "path", path }
})
}
};
QJsonDocument doc(root);
QUrl url { NetworkingConstants::METAVERSE_SERVER_URL.toString() + "/api/v1/places/" + place_id };
url.setQuery("access_token=" + accessTokenVariant->toString());
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply* reply = NetworkAccessManager::getInstance().put(req, doc.toJson());
connect(reply, &QNetworkReply::finished, this, [reply, connection]() {
if (reply->error() != QNetworkReply::NoError) {
qDebug() << "Got error response from metaverse server: " << reply->readAll();
connection->respond(HTTPConnection::StatusCode500,
"Error communicating with Metaverse");
return;
}
connection->respond(HTTPConnection::StatusCode200, reply->readAll());
});
return true;
}
} else if (connection->requestOperation() == QNetworkAccessManager::DeleteOperation) {
const QString ALL_NODE_DELETE_REGEX_STRING = QString("\\%1\\/?$").arg(URI_NODES);
const QString NODE_DELETE_REGEX_STRING = QString("\\%1\\/(%2)\\/$").arg(URI_NODES).arg(UUID_REGEX_STRING);
@ -2037,7 +2244,6 @@ static const QString HIFI_SESSION_COOKIE_KEY = "DS_WEB_SESSION_UUID";
static const QString STATE_QUERY_KEY = "state";
bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &url, bool skipSubHandler) {
qDebug() << "HTTPS request received at" << url.toString();
if (url.path() == URI_OAUTH) {
QUrlQuery codeURLQuery(url);
@ -2298,6 +2504,8 @@ QNetworkReply* DomainServer::profileRequestGivenTokenReply(QNetworkReply* tokenR
profileURL.setPath("/api/v1/user/profile");
profileURL.setQuery(QString("%1=%2").arg(OAUTH_JSON_ACCESS_TOKEN_KEY, accessToken));
qDebug() << "Sending profile request to: " << profileURL;
QNetworkRequest profileRequest(profileURL);
profileRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
profileRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);

View file

@ -187,6 +187,13 @@ private:
HTTPSConnection* connectionFromReplyWithState(QNetworkReply* reply);
bool forwardMetaverseAPIRequest(HTTPConnection* connection,
const QString& metaversePath,
const QString& requestSubobject,
std::initializer_list<QString> requiredData = { },
std::initializer_list<QString> optionalData = { },
bool requireAccessToken = true);
SubnetList _acSubnetWhitelist;
std::vector<QString> _replicatedUsernames;

View file

@ -310,6 +310,16 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
_standardAgentPermissions[NodePermissions::standardNameLocalhost]->set(NodePermissions::Permission::canRezTemporaryCertifiedEntities);
packPermissions();
}
if (oldVersion < 2.0) {
const QString WIZARD_COMPLETED_ONCE = "wizard.completed_once";
QVariant* wizardCompletedOnce = _configMap.valueForKeyPath(WIZARD_COMPLETED_ONCE, true);
*wizardCompletedOnce = QVariant(true);
// write the new settings to the json file
persistToFile();
}
}
unpackPermissions();
@ -960,29 +970,6 @@ QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QStrin
return QVariant();
}
bool DomainServerSettingsManager::handlePublicHTTPRequest(HTTPConnection* connection, const QUrl &url) {
if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH_JSON) {
// this is a GET operation for our settings
// check if there is a query parameter for settings affecting a particular type of assignment
const QString SETTINGS_TYPE_QUERY_KEY = "type";
QUrlQuery settingsQuery(url);
QString typeValue = settingsQuery.queryItemValue(SETTINGS_TYPE_QUERY_KEY);
if (!typeValue.isEmpty()) {
QJsonObject responseObject = responseObjectForType(typeValue);
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(responseObject).toJson(), "application/json");
return true;
} else {
return false;
}
}
return false;
}
bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection *connection, const QUrl &url) {
if (connection->requestOperation() == QNetworkAccessManager::PostOperation && url.path() == SETTINGS_PATH_JSON) {
// this is a POST operation to change one or more settings
@ -1214,6 +1201,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
static const QString SECURITY_ROOT_KEY = "security";
static const QString AC_SUBNET_WHITELIST_KEY = "ac_subnet_whitelist";
static const QString BROADCASTING_KEY = "broadcasting";
static const QString WIZARD_KEY = "wizard";
static const QString DESCRIPTION_ROOT_KEY = "descriptors";
auto& settingsVariant = _configMap.getConfig();
@ -1266,7 +1254,8 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
if (!matchingDescriptionObject.isEmpty()) {
updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject);
if (rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY && rootKey != SETTINGS_PATHS_KEY ) {
if (rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY &&
rootKey != SETTINGS_PATHS_KEY && rootKey != WIZARD_KEY) {
needRestart = true;
}
} else {
@ -1282,8 +1271,9 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
if (!matchingDescriptionObject.isEmpty()) {
const QJsonValue& settingValue = rootValue.toObject()[settingKey];
updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject);
if ((rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY && rootKey != DESCRIPTION_ROOT_KEY)
|| settingKey == AC_SUBNET_WHITELIST_KEY) {
if ((rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY &&
rootKey != DESCRIPTION_ROOT_KEY && rootKey != WIZARD_KEY) ||
settingKey == AC_SUBNET_WHITELIST_KEY) {
needRestart = true;
}
} else {

View file

@ -43,7 +43,6 @@ class DomainServerSettingsManager : public QObject {
Q_OBJECT
public:
DomainServerSettingsManager();
bool handlePublicHTTPRequest(HTTPConnection* connection, const QUrl& url);
bool handleAuthenticatedHTTPRequest(HTTPConnection* connection, const QUrl& url);
void setupConfigMap(const QStringList& argumentList);

View file

@ -17,6 +17,7 @@
#include "HTTPConnection.h"
#include "EmbeddedWebserverLogging.h"
#include "HTTPManager.h"
#include <QUrlQuery>
const char* HTTPConnection::StatusCode200 = "200 OK";
const char* HTTPConnection::StatusCode301 = "301 Moved Permanently";
@ -52,10 +53,31 @@ HTTPConnection::~HTTPConnection() {
}
}
QHash<QString, QString> HTTPConnection::parseUrlEncodedForm() {
// make sure we have the correct MIME type
QList<QByteArray> elements = _requestHeaders.value("Content-Type").split(';');
QString contentType = elements.at(0).trimmed();
if (contentType != "application/x-www-form-urlencoded") {
return QHash<QString, QString>();
}
QUrlQuery form { _requestContent };
QHash<QString, QString> pairs;
for (auto pair : form.queryItems()) {
pairs[QUrl::fromPercentEncoding(pair.first.toLatin1())] = QUrl::fromPercentEncoding(pair.second.toLatin1());
}
return pairs;
}
QList<FormData> HTTPConnection::parseFormData() const {
// make sure we have the correct MIME type
QList<QByteArray> elements = _requestHeaders.value("Content-Type").split(';');
if (elements.at(0).trimmed() != "multipart/form-data") {
QString contentType = elements.at(0).trimmed();
if (contentType != "multipart/form-data") {
return QList<FormData>();
}

View file

@ -79,6 +79,10 @@ public:
/// Parses the request content as form data, returning a list of header/content pairs.
QList<FormData> parseFormData () const;
/// Parses the request content as a url encoded form, returning a hash of key/value pairs.
/// Duplicate keys are not supported.
QHash<QString, QString> parseUrlEncodedForm();
/// Sends a response and closes the connection.
void respond (const char* code, const QByteArray& content = QByteArray(),
const char* contentType = DefaultContentType,