Commit 6c3afdbd authored by Volker Krause's avatar Volker Krause
Browse files

Split analytics and admin interface

This enables us to have two user roles: people with read-only access to
data for viewing/analyzing it, and people with full write access to
manage products and survey campaigns.
parent 7f0672e9
......@@ -7,7 +7,11 @@ The usual cmake/make/make install.
Requires PHP >= 5.5 and Sqlite, MySQL or PostgreSQL, Apache with SSL set up.
- copy src/server to your webserver (alternatively, use "make deploy")
- set up authentication for the analytics sub-folder
- make sure .htaccess files are enabled by your Apache settings
- set up authentication:
- users with access to the analytics sub-folder have read-only access to all product settings
and telemetry data
- users with additional access to the admin sub-folder have write access to all products
- rename config/localconfig.php.example to config/localconfig.php and adjust
settings in there based on your database setup
- start UserFeedbackConsole, connect to the server, that will trigger the database
......
......@@ -61,7 +61,7 @@ void HandshakeJob::processResponse(QNetworkReply* reply)
const auto obj = doc.object();
const auto protoVer = obj.value(QLatin1String("protocolVersion")).toInt();
if (protoVer != 1) {
if (protoVer != 2) {
emitError(tr("Incompatbile protcol: %1.").arg(protoVer));
return;
}
......
......@@ -28,62 +28,62 @@ using namespace UserFeedback::Console;
QNetworkReply* RESTApi::checkSchema(RESTClient *client)
{
return client->get(QStringLiteral("check_schema"));
return client->get(QStringLiteral("analytics/check_schema"));
}
QNetworkReply* RESTApi::listProducts(RESTClient *client)
{
return client->get(QStringLiteral("products"));
return client->get(QStringLiteral("analytics/products"));
}
QNetworkReply* RESTApi::createProduct(RESTClient *client, const Product &p)
{
Q_ASSERT(p.isValid());
return client->post(QStringLiteral("products"), p.toJson());
return client->post(QStringLiteral("admin/products"), p.toJson());
}
QNetworkReply* RESTApi::updateProduct(RESTClient *client, const Product &p)
{
Q_ASSERT(p.isValid());
return client->put(QStringLiteral("products/") + p.name(), p.toJson());
return client->put(QStringLiteral("admin/products/") + p.name(), p.toJson());
}
QNetworkReply* RESTApi::deleteProduct(RESTClient *client, const Product &p)
{
Q_ASSERT(p.isValid());
return client->deleteResource(QStringLiteral("products/") + p.name());
return client->deleteResource(QStringLiteral("admin/products/") + p.name());
}
QNetworkReply* RESTApi::listSamples(RESTClient *client, const Product &p)
{
Q_ASSERT(p.isValid());
return client->get(QStringLiteral("data/") + p.name());
return client->get(QStringLiteral("analytics/data/") + p.name());
}
QNetworkReply* RESTApi::addSamples(RESTClient *client, const Product &p, const QVector<Sample> &samples)
{
Q_ASSERT(p.isValid());
return client->post(QStringLiteral("data/") + p.name(), Sample::toJson(samples, p));
return client->post(QStringLiteral("admin/data/") + p.name(), Sample::toJson(samples, p));
}
QNetworkReply* RESTApi::listSurveys(RESTClient *client, const Product &p)
{
Q_ASSERT(p.isValid());
return client->get(QStringLiteral("surveys/") + p.name());
return client->get(QStringLiteral("analytics/surveys/") + p.name());
}
QNetworkReply* RESTApi::createSurvey(RESTClient *client, const Product &p, const Survey &s)
{
Q_ASSERT(p.isValid());
return client->post(QStringLiteral("surveys/") + p.name(), s.toJson());
return client->post(QStringLiteral("admin/surveys/") + p.name(), s.toJson());
}
QNetworkReply* RESTApi::updateSurvey(RESTClient *client, const Survey &s)
{
return client->put(QStringLiteral("surveys/") + s.uuid().toString(), s.toJson());
return client->put(QStringLiteral("admin/surveys/") + s.uuid().toString(), s.toJson());
}
QNetworkReply* RESTApi::deleteSurvey(RESTClient *client, const Survey &s)
{
return client->deleteResource(QStringLiteral("surveys/") + s.uuid().toString());
return client->deleteResource(QStringLiteral("admin/surveys/") + s.uuid().toString());
}
......@@ -98,7 +98,7 @@ QNetworkRequest RESTClient::makeRequest(const QString& command)
auto path = url.path();
if (!path.endsWith(QLatin1Char('/')))
path += QLatin1Char('/');
path += QStringLiteral("analytics/") + command;
path += command;
url.setPath(path);
QNetworkRequest request(url);
const QByteArray authToken = m_serverInfo.userName().toUtf8() + ':' + m_serverInfo.password().toUtf8();
......
......@@ -2,6 +2,11 @@ set(server_top_srcs
.htaccess
)
set(server_admin_srcs
admin/index.php
admin/.htaccess
)
set(server_analytics_srcs
analytics/index.php
analytics/.htaccess
......@@ -12,6 +17,7 @@ set(server_config_srcs
)
set(server_shared_srcs
shared/admin.php
shared/aggregation.php
shared/analytics.php
shared/compat.php
......@@ -34,6 +40,7 @@ set(server_receiver_srcs
receiver/.htaccess
)
php_lint(${server_admin_srcs})
php_lint(${server_analytics_srcs})
php_lint(${server_config_srcs})
php_lint(${server_shared_srcs})
......@@ -42,6 +49,7 @@ php_lint(${server_receiver_srcs})
if (SERVER_DEPLOY_DESTINATION)
add_custom_target(deploy
COMMAND scp ${server_top_srcs} ${SERVER_DEPLOY_DESTINATION}/
COMMAND scp ${server_admin_srcs} ${SERVER_DEPLOY_DESTINATION}/admin
COMMAND scp ${server_analytics_srcs} ${SERVER_DEPLOY_DESTINATION}/analytics
COMMAND scp ${server_config_srcs} ${SERVER_DEPLOY_DESTINATION}/config
COMMAND scp ${server_receiver_srcs} ${SERVER_DEPLOY_DESTINATION}/receiver
......
Allow from all
Options +FollowSymLinks
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]
<?php
/*
Copyright (C) 2017 Volker Krause <vkrause@kde.org>
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
include_once(__DIR__ . '/../shared/restdispatcher.php');
include_once(__DIR__ . '/../shared/admin.php');
$handler = new Admin();
RESTDispatcher::dispatch($handler);
?>
<?php
/*
Copyright (C) 2016 Volker Krause <vkrause@kde.org>
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
require_once('datastore.php');
require_once('product.php');
require_once('sample.php');
require_once('survey.php');
require_once('utils.php');
/** Command handler for the admin interface. */
class Admin
{
/** Add a new product. */
public function post_products()
{
$rawPostData = file_get_contents('php://input');
$product = Product::fromJson($rawPostData);
$db = new DataStore();
$db->beginTransaction();
$product->insert($db);
$db->commit();
echo('Product ' . $product->name . " added.");
}
/** Update a given product. */
public function put_products($productName)
{
$raw = file_get_contents('php://input');
$newProduct = Product::fromJson($raw);
$db = new DataStore();
$db->beginTransaction();
$oldProduct = Product::productByName($db, $productName);
if (is_null($oldProduct))
throw new RESTException('Product not found.', 404);
$oldProduct->update($db, $newProduct);
$db->commit();
echo('Product ' . $productName . ' updated.');
}
/** Delete product and associated data. */
public function delete_products($productName)
{
$db = new DataStore();
$db->beginTransaction();
$product = Product::productByName($db, $productName);
if (is_null($product))
throw new RESTException('Product not found.', 404);
$product->delete($db);
$db->commit();
echo('Product ' . $productName . ' deleted.');
}
/** Import data for a product. */
public function post_data($productName)
{
$db = new DataStore();
$db->beginTransaction();
$product = Product::productByName($db, $productName);
if (is_null($product))
throw RESTException('Unknown product.', 404);
Sample::import($db, file_get_contents('php://input'), $product);
$db->commit();
echo('Data imported.');
}
/** Add new survey. */
public function post_surveys($productName)
{
if ($productName == "")
Utils::httpError(400, "No product id specified.");
$rawPostData = file_get_contents('php://input');
$survey = Survey::fromJson($rawPostData);
$db = new DataStore();
$db->beginTransaction();
$product = Product::productByName($db, $productName);
if (is_null($product))
Utils::httpError(404, "Invalid product identifier.");
$survey->insert($db, $product);
$db->commit();
echo('Survey created for product ' . $product->name . '.');
}
/** Edit an existing survey. */
public function put_surveys($surveyId)
{
$surveyId = strval($surveyId);
if (strlen($surveyId) <= 0)
throw new RESTException('Invalid survey id.', 400);
$surveyData = file_get_contents('php://input');
$survey = Survey::fromJson($surveyData);
$survey->uuid = $surveyId;
$db = new DataStore();
$db->beginTransaction();
$survey->update($db);
$db->commit();
echo("Survey updated.");
}
/** Delete survey. */
public function delete_surveys($surveyId)
{
$survey = new Survey;
$survey->uuid = strval($surveyId);
if (strlen($survey->uuid) <= 0)
throw new RESTException('Invalid survey id.', 400);
$db = new DataStore();
$db->beginTransaction();
$survey->delete($db);
$db->commit();
echo("Survey deleted.");
}
}
?>
......@@ -36,7 +36,7 @@ public function get_check_schema()
// check database layout
$db = new DataStore();
$res = $db->checkSchema();
$res['protocolVersion'] = 1;
$res['protocolVersion'] = 2;
header('Content-Type: text/json');
echo(json_encode($res));
......@@ -53,50 +53,6 @@ public function get_products()
echo($json);
}
/** Add a new product. */
public function post_products()
{
$rawPostData = file_get_contents('php://input');
$product = Product::fromJson($rawPostData);
$db = new DataStore();
$db->beginTransaction();
$product->insert($db);
$db->commit();
echo('Product ' . $product->name . " added.");
}
/** Update a given product. */
public function put_products($productName)
{
$raw = file_get_contents('php://input');
$newProduct = Product::fromJson($raw);
$db = new DataStore();
$db->beginTransaction();
$oldProduct = Product::productByName($db, $productName);
if (is_null($oldProduct))
throw new RESTException('Product not found.', 404);
$oldProduct->update($db, $newProduct);
$db->commit();
echo('Product ' . $productName . ' updated.');
}
/** Delete product and associated data. */
public function delete_products($productName)
{
$db = new DataStore();
$db->beginTransaction();
$product = Product::productByName($db, $productName);
if (is_null($product))
throw new RESTException('Product not found.', 404);
$product->delete($db);
$db->commit();
echo('Product ' . $productName . ' deleted.');
}
/** List data for a product. */
public function get_data($productName)
{
......@@ -109,19 +65,6 @@ public function get_data($productName)
echo(Sample::dataAsJson($db, $product));
}
/** Import data for a product. */
public function post_data($productName)
{
$db = new DataStore();
$db->beginTransaction();
$product = Product::productByName($db, $productName);
if (is_null($product))
throw RESTException('Unknown product.', 404);
Sample::import($db, file_get_contents('php://input'), $product);
$db->commit();
echo('Data imported.');
}
/** List all surveys for a product. */
public function get_surveys($productName)
{
......@@ -135,59 +78,6 @@ public function get_surveys($productName)
echo(json_encode($surveys));
}
/** Add new survey. */
public function post_surveys($productName)
{
if ($productName == "")
Utils::httpError(400, "No product id specified.");
$rawPostData = file_get_contents('php://input');
$survey = Survey::fromJson($rawPostData);
$db = new DataStore();
$db->beginTransaction();
$product = Product::productByName($db, $productName);
if (is_null($product))
Utils::httpError(404, "Invalid product identifier.");
$survey->insert($db, $product);
$db->commit();
echo('Survey created for product ' . $product->name . '.');
}
/** Edit an existing survey. */
public function put_surveys($surveyId)
{
$surveyId = strval($surveyId);
if (strlen($surveyId) <= 0)
throw new RESTException('Invalid survey id.', 400);
$surveyData = file_get_contents('php://input');
$survey = Survey::fromJson($surveyData);
$survey->uuid = $surveyId;
$db = new DataStore();
$db->beginTransaction();
$survey->update($db);
$db->commit();
echo("Survey updated.");
}
/** Delete survey. */
public function delete_surveys($surveyId)
{
$survey = new Survey;
$survey->uuid = strval($surveyId);
if (strlen($survey->uuid) <= 0)
throw new RESTException('Invalid survey id.', 400);
$db = new DataStore();
$db->beginTransaction();
$survey->delete($db);
$db->commit();
echo("Survey deleted.");
}
}
?>
......@@ -25,6 +25,10 @@ $USERFEEDBACK_DB_NAME = __DIR__ . '/../server/data/db.sqlite';
$path = explode('/', $_SERVER['REQUEST_URI']);
switch ($path[1]) {
case 'admin':
$_SERVER['PHP_SELF'] = '/admin/index.php';
include '../server/admin/index.php';
return;
case 'analytics':
$_SERVER['PHP_SELF'] = '/analytics/index.php';
include '../server/analytics/index.php';
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment