A LOT of improvements

This commit is contained in:
Maximilian Giller 2020-11-08 15:21:51 +01:00
parent 5c30876c80
commit 4c64f99ced
26 changed files with 1307 additions and 861 deletions

8
.gitignore vendored
View file

@ -1,5 +1,5 @@
juggl/config/config.txt juggl/config/config.txt
juggl/config/config.path juggl/config/config.path
juggl/config/config.php juggl/config/config.php
graphics graphics
.vscode .vscode

View file

@ -1,8 +1,12 @@
<?php <?php
$config = [ ini_set('display_errors', 1);
"host" => "localhost", ini_set('display_startup_errors', 1);
"dbname" => "juggl", error_reporting(E_ALL | E_STRICT);
"username" => "juggl",
"password" => "?=5,}f_F&){;@xthx-[i", $config = [
"table_prefix" => "ju_" "host" => "localhost",
]; "dbname" => "juggl",
"username" => "juggl",
"password" => "?=5,}f_F&){;@xthx-[i",
"table_prefix" => "ju_"
];

View file

@ -1,8 +1,8 @@
<?php <?php
$config = [ $config = [
"host" => "", "host" => "",
"dbname" => "", "dbname" => "",
"username" => "", "username" => "",
"password" => "", "password" => "",
"table_prefix" => "ju_" "table_prefix" => "ju_"
]; ];

View file

@ -1,28 +1,27 @@
<?php <?php
session_start(); session_start();
require_once(__DIR__ . "/services/apiBranch.inc.php"); require_once(__DIR__ . "/services/apiBranch.inc.php");
require_once(__DIR__ . "/services/responses.inc.php"); require_once(__DIR__ . "/services/responses.inc.php");
require_once(__DIR__ . "/services/jugglDbApi.inc.php"); require_once(__DIR__ . "/services/jugglDbApi.inc.php");
class EndRecordBranch extends ApiBranch class EndRecordBranch extends ApiBranch
{ {
function get(ParamCleaner $params) function get(ParamCleaner $params)
{ {
respondStatus(405); respondStatus(405);
} }
function post(ParamCleaner $params) function post(ParamCleaner $params)
{ {
$user_id = $params->get("user_id"); $user_id = $params->get("user_id");
$params->select("request");
if ($params->exists(["end_time", "record_id"]) == false) {
if ($params->exists(["end_time", "record_id"]) == false) { respondStatus(400, "Missing parameter");
respondStatus(400, "Missing parameter"); }
}
updateEndRecord($user_id, $params);
updateEndRecord($user_id, $params); }
} }
}
$branch = new EndRecordBranch();
$branch = new EndRecordBranch(); $branch->execute();
$branch->execute();

View file

@ -0,0 +1,29 @@
<?php
session_start();
require_once(__DIR__ . "/services/apiBranch.inc.php");
require_once(__DIR__ . "/services/jsonBuilder.inc.php");
require_once(__DIR__ . "/services/responses.inc.php");
require_once(__DIR__ . "/services/jugglDbApi.inc.php");
class GetProjectsBranch extends ApiBranch
{
function get(ParamCleaner $params)
{
respondStatus(405);
}
function post(ParamCleaner $params)
{
$user_id = $params->get("user_id");
$projects = getProjects($user_id);
$json = new JsonBuilder();
$json->addProjects($projects);
respondJson($json);
}
}
$branch = new GetProjectsBranch();
$branch->execute();

View file

@ -1,34 +1,33 @@
<?php <?php
session_start(); session_start();
require_once(__DIR__ . "/services/apiBranch.inc.php"); require_once(__DIR__ . "/services/apiBranch.inc.php");
require_once(__DIR__ . "/services/jsonBuilder.inc.php"); require_once(__DIR__ . "/services/jsonBuilder.inc.php");
require_once(__DIR__ . "/services/responses.inc.php"); require_once(__DIR__ . "/services/responses.inc.php");
require_once(__DIR__ . "/services/jugglDbApi.inc.php"); require_once(__DIR__ . "/services/jugglDbApi.inc.php");
class GetRecordBranch extends ApiBranch class GetRecordBranch extends ApiBranch
{ {
function get(ParamCleaner $params) function get(ParamCleaner $params)
{ {
respondStatus(405); respondStatus(405);
} }
function post(ParamCleaner $params) function post(ParamCleaner $params)
{ {
$user_id = $params->get("user_id"); $user_id = $params->get("user_id");
$params->select("request");
if ($params->exists(["record_id"]) == false) {
if ($params->exists(["record_id"]) == false) { respondStatus(400, "Missing parameter");
respondStatus(400, "Missing parameter"); }
}
$record = getTimeRecord($user_id, $params->get("record_id"));
$record = getTimeRecord($user_id, $params->get("record_id"));
$json = new JsonBuilder();
$json = new JsonBuilder(); $json->addRecords([$record]);
$json->addRecords([$record]);
respondJson($json);
respondJson($json); }
} }
}
$branch = new GetRecordBranch();
$branch = new GetRecordBranch(); $branch->execute();
$branch->execute();

View file

@ -1,38 +1,38 @@
<?php <?php
require_once(__DIR__."/authenticator.inc.php"); require_once(__DIR__."/authenticator.inc.php");
require_once(__DIR__."/responses.inc.php"); require_once(__DIR__."/responses.inc.php");
require_once(__DIR__."/requestTypes.inc.php"); require_once(__DIR__."/requestTypes.inc.php");
require_once(__DIR__."/paramCleaner.inc.php"); require_once(__DIR__."/paramCleaner.inc.php");
abstract class ApiBranch { abstract class ApiBranch {
function get (ParamCleaner $params) {} function get (ParamCleaner $params) {}
function post (ParamCleaner $params) {} function post (ParamCleaner $params) {}
function authenticationMissing (ParamCleaner $params) { function authenticationMissing (ParamCleaner $params) {
respondStatus(403); respondStatus(403);
} }
function execute ($authenticationRequired = true) { function execute ($authenticationRequired = true) {
$params = $this->getParams(); $params = $this->getParams();
if ($authenticationRequired) { if ($authenticationRequired) {
$auth = new Authenticator(); $auth = new Authenticator();
if (!$auth->isAuthenticated($params)) { if (!$auth->isAuthenticated($params)) {
$this->authenticationMissing($params); $this->authenticationMissing($params);
return; return;
} }
} }
$currentType = currentRequestType(); $currentType = currentRequestType();
if($currentType === RequestType::GET) { if($currentType === RequestType::GET) {
$this->get($params); $this->get($params);
} else if ($currentType === RequestType::POST) { } else if ($currentType === RequestType::POST) {
$this->post($params); $this->post($params);
} }
} }
private function getParams() { private function getParams() {
$content = json_decode(file_get_contents('php://input'), true); $content = json_decode(file_get_contents('php://input'), true);
return new ParamCleaner(array_merge($content, $_REQUEST, $_SESSION, $_FILES)); return new ParamCleaner(array_merge($content, $_REQUEST, $_SESSION, $_FILES));
} }
} }
?> ?>

View file

@ -1,20 +1,20 @@
<?php <?php
require_once(__DIR__."/dbOperations.inc.php"); require_once(__DIR__."/dbOperations.inc.php");
class Authenticator { class Authenticator {
function isApiKeyAuthenticated($api_key, $user_id) { function isApiKeyAuthenticated($api_key, $user_id) {
$db = new DbOperations(); $db = new DbOperations();
$db->select("api_keys", ["enabled"]); $db->select("api_keys", ["enabled"]);
$db->where("api_key", Comparison::EQUAL, $api_key); $db->where("api_key", Comparison::EQUAL, $api_key);
$db->where("user_id", Comparison::EQUAL, $user_id); $db->where("user_id", Comparison::EQUAL, $user_id);
$result = $db->execute(); $result = $db->execute();
return count($result) == 1 && $result[0]['enabled']; return count($result) == 1 && $result[0]['enabled'];
} }
function isAuthenticated($params) { function isAuthenticated($params) {
return $this->isApiKeyAuthenticated($params->get('api_key'), $params->get('user_id')); return $this->isApiKeyAuthenticated($params->get('api_key'), $params->get('user_id'));
} }
} }
?> ?>

View file

@ -1,33 +1,33 @@
<?php <?php
abstract class BasicEnum { abstract class BasicEnum {
private static $constCacheArray = NULL; private static $constCacheArray = NULL;
private static function getConstants() { private static function getConstants() {
if (self::$constCacheArray == NULL) { if (self::$constCacheArray == NULL) {
self::$constCacheArray = []; self::$constCacheArray = [];
} }
$calledClass = get_called_class(); $calledClass = get_called_class();
if (!array_key_exists($calledClass, self::$constCacheArray)) { if (!array_key_exists($calledClass, self::$constCacheArray)) {
$reflect = new ReflectionClass($calledClass); $reflect = new ReflectionClass($calledClass);
self::$constCacheArray[$calledClass] = $reflect->getConstants(); self::$constCacheArray[$calledClass] = $reflect->getConstants();
} }
return self::$constCacheArray[$calledClass]; return self::$constCacheArray[$calledClass];
} }
public static function isValidName($name, $strict = false) { public static function isValidName($name, $strict = false) {
$constants = self::getConstants(); $constants = self::getConstants();
if ($strict) { if ($strict) {
return array_key_exists($name, $constants); return array_key_exists($name, $constants);
} }
$keys = array_map('strtolower', array_keys($constants)); $keys = array_map('strtolower', array_keys($constants));
return in_array(strtolower($name), $keys); return in_array(strtolower($name), $keys);
} }
public static function isValidValue($value, $strict = true) { public static function isValidValue($value, $strict = true) {
$values = array_values(self::getConstants()); $values = array_values(self::getConstants());
return in_array($value, $values, $strict); return in_array($value, $values, $strict);
} }
} }
?> ?>

View file

@ -1,291 +1,291 @@
<?php <?php
require_once(__DIR__ . "/basicEnum.inc.php"); require_once(__DIR__ . "/basicEnum.inc.php");
class DbOperations class DbOperations
{ {
function __construct($tablePrefix = null) function __construct($tablePrefix = null)
{ {
$this->resetQuery(); $this->resetQuery();
$this->tablePrefix = $tablePrefix; $this->tablePrefix = $tablePrefix;
require(__DIR__ . "/../config/config.php"); require(__DIR__ . "/../config/config.php");
$this->config = $config; $this->config = $config;
if ($this->tablePrefix == null) { if ($this->tablePrefix == null) {
$this->tablePrefix = $this->config["table_prefix"]; $this->tablePrefix = $this->config["table_prefix"];
} }
} }
function resetQuery() function resetQuery()
{ {
$this->query = ""; $this->query = "";
$this->data = array(); $this->data = array();
$this->table = ""; $this->table = "";
} }
private function openConnection() private function openConnection()
{ {
$host = $this->config['host']; $host = $this->config['host'];
$dbname = $this->config['dbname']; $dbname = $this->config['dbname'];
$dsn = "mysql:host=$host;dbname=$dbname"; $dsn = "mysql:host=$host;dbname=$dbname";
$options = array(PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC); $options = array(PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC);
$this->pdo = new PDO($dsn, $this->config['username'], $this->config['password'], $options); $this->pdo = new PDO($dsn, $this->config['username'], $this->config['password'], $options);
} }
function select(string $table, array $attributes = array()) function select(string $table, array $attributes = array())
{ {
$this->table = $this->tablePrefix . $table; $this->table = $this->tablePrefix . $table;
if (count($attributes) == 0) if (count($attributes) == 0)
$formattedAttributes = "*"; $formattedAttributes = "*";
else { else {
for ($i = 0; $i < count($attributes); $i++) { for ($i = 0; $i < count($attributes); $i++) {
$a = $attributes[$i]; $a = $attributes[$i];
if (strpos($a, ".") === false) { if (strpos($a, ".") === false) {
$attributes[$i] = "$this->table.$a"; $attributes[$i] = "$this->table.$a";
} }
} }
$formattedAttributes = implode(', ', $attributes); $formattedAttributes = implode(', ', $attributes);
} }
$this->addToQuery("SELECT $formattedAttributes FROM $this->tablePrefix$table"); $this->addToQuery("SELECT $formattedAttributes FROM $this->tablePrefix$table");
return $this; return $this;
} }
function orderBy(string $attribute, string $order = Order::ASC) function orderBy(string $attribute, string $order = Order::ASC)
{ {
$this->addToQuery("ORDER BY $attribute $order"); $this->addToQuery("ORDER BY $attribute $order");
return $this; return $this;
} }
static function getLatestIdInTable(string $table, string $attribute = "id") static function getLatestIdInTable(string $table, string $attribute = "id")
{ {
$db = new DbOperations(); $db = new DbOperations();
$db->select($table, array($attribute)); $db->select($table, array($attribute));
$db->orderBy($attribute, Order::DESC); $db->orderBy($attribute, Order::DESC);
return $db->execute()[0][$attribute]; return $db->execute()[0][$attribute];
} }
function insert(string $table, array $data) function insert(string $table, array $data)
{ {
$this->table = $this->tablePrefix . $table; $this->table = $this->tablePrefix . $table;
$attributes = implode(", ", array_keys($data)); $attributes = implode(", ", array_keys($data));
$valuesIds = array(); $valuesIds = array();
foreach ($data as $attribute => $value) { foreach ($data as $attribute => $value) {
$valuesIds[] = $this->addData($value, $attribute); $valuesIds[] = $this->addData($value, $attribute);
} }
$values = implode(" , ", $valuesIds); $values = implode(" , ", $valuesIds);
$this->addToQuery("INSERT INTO $this->tablePrefix$table ( $attributes ) VALUES ( $values )"); $this->addToQuery("INSERT INTO $this->tablePrefix$table ( $attributes ) VALUES ( $values )");
return $this; return $this;
} }
function insertMultiple(string $table, array $attributes, array $data) function insertMultiple(string $table, array $attributes, array $data)
{ {
$this->table = $this->tablePrefix . $table; $this->table = $this->tablePrefix . $table;
$attributesString = implode(", ", $attributes); $attributesString = implode(", ", $attributes);
$valueGroups = array(); $valueGroups = array();
$groupIndex = 0; // To avoid same value ids $groupIndex = 0; // To avoid same value ids
foreach ($data as $dataGroup) { foreach ($data as $dataGroup) {
if (sizeof($attributes) != sizeof($dataGroup)) { if (sizeof($attributes) != sizeof($dataGroup)) {
continue; continue;
} }
$valueIds = array(); $valueIds = array();
// Indexed for used, so that attributes can easily be assigned to the according values // Indexed for used, so that attributes can easily be assigned to the according values
for ($i = 0; $i < sizeof($dataGroup); $i++) { for ($i = 0; $i < sizeof($dataGroup); $i++) {
$valueIds[] = $this->addData($dataGroup[$i], $attributes[$i] . "_" . (string) $groupIndex); $valueIds[] = $this->addData($dataGroup[$i], $attributes[$i] . "_" . (string) $groupIndex);
} }
$valueGroups[] = "(" . implode(", ", $valueIds) . ")"; $valueGroups[] = "(" . implode(", ", $valueIds) . ")";
$groupIndex++; $groupIndex++;
} }
$values = implode(", ", $valueGroups); $values = implode(", ", $valueGroups);
$this->addToQuery("INSERT INTO $this->tablePrefix$table ( $attributesString ) VALUES $values"); $this->addToQuery("INSERT INTO $this->tablePrefix$table ( $attributesString ) VALUES $values");
return $this; return $this;
} }
function update(string $table, array $data) function update(string $table, array $data)
{ {
$this->table = $this->tablePrefix . $table; $this->table = $this->tablePrefix . $table;
$sets = array(); $sets = array();
foreach ($data as $attribute => $value) { foreach ($data as $attribute => $value) {
$valueId = $this->addData($value, $attribute); $valueId = $this->addData($value, $attribute);
$sets[] = "$attribute = $valueId"; $sets[] = "$attribute = $valueId";
} }
$setString = implode(", ", $sets); $setString = implode(", ", $sets);
$this->addToQuery("UPDATE $this->tablePrefix$table SET $setString"); $this->addToQuery("UPDATE $this->tablePrefix$table SET $setString");
return $this; return $this;
} }
function delete(string $table) function delete(string $table)
{ {
$this->table = $this->tablePrefix . $table; $this->table = $this->tablePrefix . $table;
$this->addToQuery("DELETE FROM $this->tablePrefix$table"); $this->addToQuery("DELETE FROM $this->tablePrefix$table");
return $this; return $this;
} }
function limit(int $limit, int $offset = null) function limit(int $limit, int $offset = null)
{ {
$this->addToQuery("LIMIT $limit"); $this->addToQuery("LIMIT $limit");
if ($offset != null) { if ($offset != null) {
$this->addToQuery("OFFSET $offset"); $this->addToQuery("OFFSET $offset");
} }
return $this; return $this;
} }
private function addToQuery(string $phrase) private function addToQuery(string $phrase)
{ {
$delimeter = " "; $delimeter = " ";
$this->query = implode($delimeter, array($this->query, $phrase)); $this->query = implode($delimeter, array($this->query, $phrase));
} }
function where(string $attribute, string $comparison, $value, string $connector = Combination::AND) function where(string $attribute, string $comparison, $value, string $connector = Combination::AND)
{ {
if (Comparison::isValidValue($comparison) == false) if (Comparison::isValidValue($comparison) == false)
return; return;
$keyWord = "WHERE"; $keyWord = "WHERE";
if (!(strpos($this->query, $keyWord) === false)) if (!(strpos($this->query, $keyWord) === false))
$keyWord = $connector; $keyWord = $connector;
$valueId = $this->addData($value, $attribute); $valueId = $this->addData($value, $attribute);
$this->addToQuery("$keyWord $attribute $comparison $valueId"); $this->addToQuery("$keyWord $attribute $comparison $valueId");
return $this; return $this;
} }
function whereOneOf(string $attribute, string $comparison, $values, string $connector = Combination::AND) function whereOneOf(string $attribute, string $comparison, $values, string $connector = Combination::AND)
{ {
if (Comparison::isValidValue($comparison) == false) if (Comparison::isValidValue($comparison) == false)
return; return;
$keyWord = "WHERE"; $keyWord = "WHERE";
if (!(strpos($this->query, $keyWord) === false)) if (!(strpos($this->query, $keyWord) === false))
$keyWord = $connector; $keyWord = $connector;
$whereClause = "$keyWord ( "; $whereClause = "$keyWord ( ";
for ($i = 0; $i < sizeof($values); $i++) { for ($i = 0; $i < sizeof($values); $i++) {
if ($i > 0) { if ($i > 0) {
$whereClause .= " OR "; $whereClause .= " OR ";
} }
$valueId = $this->addData($values[$i], $attribute . '_' . $i); $valueId = $this->addData($values[$i], $attribute . '_' . $i);
$whereClause .= "$attribute $comparison $valueId"; $whereClause .= "$attribute $comparison $valueId";
} }
$whereClause .= " )"; $whereClause .= " )";
$this->addToQuery($whereClause); $this->addToQuery($whereClause);
return $this; return $this;
} }
function innerJoin(string $table, string $externAttribute, string $internAttribute = "", string $internTable = "") function innerJoin(string $table, string $externAttribute, string $internAttribute = "", string $internTable = "")
{ {
if ($internTable === "") { if ($internTable === "") {
$internTable = substr($this->table, sizeof($this->tablePrefix)); $internTable = substr($this->table, sizeof($this->tablePrefix));
} }
if ($internAttribute === "") { if ($internAttribute === "") {
$internAttribute = $externAttribute; $internAttribute = $externAttribute;
} }
$innerJoin = "INNER JOIN $this->tablePrefix$table ON $this->tablePrefix$table.$externAttribute = $this->tablePrefix$internTable.$internAttribute"; $innerJoin = "INNER JOIN $this->tablePrefix$table ON $this->tablePrefix$table.$externAttribute = $this->tablePrefix$internTable.$internAttribute";
$this->addToQuery($innerJoin); $this->addToQuery($innerJoin);
return $this; return $this;
} }
private function addData($data, $attribute) private function addData($data, $attribute)
{ {
$name = str_replace(".", "", $attribute); $name = str_replace(".", "", $attribute);
$this->data[$name] = $data; $this->data[$name] = $data;
return ":" . $name; return ":" . $name;
} }
function addSql($sql) function addSql($sql)
{ {
$this->addToQuery($sql); $this->addToQuery($sql);
} }
function addValue($value) function addValue($value)
{ {
$identifier = "customIdentifier" . $this->customValueId; $identifier = "customIdentifier" . $this->customValueId;
$this->customValueId += 1; $this->customValueId += 1;
$this->addToQuery($this->addData($value, $identifier)); $this->addToQuery($this->addData($value, $identifier));
} }
function execute() function execute()
{ {
try { try {
$this->openConnection(); $this->openConnection();
$pdoQuery = $this->pdo->prepare($this->query); $pdoQuery = $this->pdo->prepare($this->query);
$pdoQuery->execute($this->data); $pdoQuery->execute($this->data);
$results = array(); $results = array();
while ($row = $pdoQuery->fetch()) { while ($row = $pdoQuery->fetch()) {
$results[] = $row; $results[] = $row;
} }
$this->resetQuery(); $this->resetQuery();
return $results; return $results;
} catch (PDOException $e) { } catch (PDOException $e) {
// TODO: Hide errors from user and log them // TODO: Hide errors from user and log them
print($e); print($e);
return array(); return array();
} }
} }
function sql(string $sqlStatement, array $data) function sql(string $sqlStatement, array $data)
{ {
$this->query = $sqlStatement; $this->query = $sqlStatement;
foreach ($data as $attribute => $value) { foreach ($data as $attribute => $value) {
$this->addData($value, $attribute); $this->addData($value, $attribute);
} }
} }
} }
abstract class Comparison extends BasicEnum abstract class Comparison extends BasicEnum
{ {
const EQUAL = "="; const EQUAL = "=";
const GREATER_THAN = ">"; const GREATER_THAN = ">";
const GREATER_THAN_OR_EQUAL = ">="; const GREATER_THAN_OR_EQUAL = ">=";
const LESS_THAN = "<"; const LESS_THAN = "<";
const LESS_THAN_OR_EQUAL = "<="; const LESS_THAN_OR_EQUAL = "<=";
const UNEQUAL = "!="; const UNEQUAL = "!=";
const LIKE = "LIKE"; const LIKE = "LIKE";
} }
abstract class Combination extends BasicEnum abstract class Combination extends BasicEnum
{ {
const AND = "AND"; const AND = "AND";
const OR = "OR"; const OR = "OR";
} }
abstract class Order extends BasicEnum abstract class Order extends BasicEnum
{ {
const ASC = "ASC"; const ASC = "ASC";
const DESC = "DESC"; const DESC = "DESC";
} }

View file

@ -1,49 +1,67 @@
<?php <?php
class JsonBuilder class JsonBuilder
{ {
function __construct() function __construct()
{ {
$this->jsonData = array(); $this->jsonData = array();
} }
function getJson() function getJson()
{ {
return json_encode($this->jsonData, JSON_FORCE_OBJECT); return json_encode($this->jsonData, JSON_FORCE_OBJECT);
} }
function getArray() function getArray()
{ {
return $this->jsonData; return $this->jsonData;
} }
function addRecords(array $records) function addRecords(array $records)
{ {
$columns = array( $columns = array(
"record_id" => "", "record_id" => "",
"start_time" => "", "start_time" => "",
"end_time" => "", "end_time" => "",
"duration" => "", "duration" => "",
"user_id" => "", "user_id" => "",
"project_id" => "", "project_id" => "",
"start_device_id" => "" "start_device_id" => ""
); );
foreach ($records as $record) { $this->jsonData['records'] = array();
$this->jsonData['records'] = array(); foreach ($records as $record) {
$this->jsonData['records'][] = $this->createJsonArray($record, $columns); $this->jsonData['records'][] = $this->createJsonArray($record, $columns);
} }
return $this; return $this;
} }
private function createJsonArray(array $data, array $columns) function addProjects(array $projects)
{ {
$jsonArray = array(); $columns = array(
foreach ($columns as $key => $column) { "project_id" => "",
if ($column === "") { "name" => "",
$column = $key; "user_id" => "",
} "start_date" => "",
$jsonArray[$key] = $data[$column]; "duration" => "",
} "record_count" => ""
return $jsonArray; );
}
} $this->jsonData['projects'] = array();
foreach ($projects as $project) {
$this->jsonData['projects'][] = $this->createJsonArray($project, $columns);
}
return $this;
}
private function createJsonArray(array $data, array $columns)
{
$jsonArray = array();
foreach ($columns as $key => $column) {
if ($column === "") {
$column = $key;
}
$jsonArray[$key] = $data[$column];
}
return $jsonArray;
}
}

View file

@ -1,167 +1,206 @@
<?php <?php
require_once(__DIR__ . "/services/dbOperations.inc.php"); require_once(__DIR__ . "/dbOperations.inc.php");
function addStartRecord($user_id, $params, $project_id = null, $start_device_id = null) function addStartRecord($user_id, $params, $project_id = null, $start_device_id = null)
{ {
$data = [ $data = [
"user_id" => $user_id, "user_id" => $user_id,
"start_time" => $params->get("start_time"), "start_time" => $params->get("start_time"),
"project_id" => $project_id, "project_id" => $project_id,
"start_device_id" => $start_device_id "start_device_id" => $start_device_id
]; ];
$db = new DbOperations(); $db = new DbOperations();
$db->insert("time_records", $data); $db->insert("time_records", $data);
$db->execute(); $db->execute();
} }
function addTimeRecord($user_id, $params, $project_id = null, $start_device_id = null) function addTimeRecord($user_id, $params, $project_id = null, $start_device_id = null)
{ {
$data = [ $data = [
"user_id" => $user_id, "user_id" => $user_id,
"start_time" => $params->get("start_time"), "start_time" => $params->get("start_time"),
"end_time" => $params->get("end_time"), "end_time" => $params->get("end_time"),
"duration" => $params->get("duration"), "duration" => $params->get("duration"),
"project_id" => $project_id, "project_id" => $project_id,
"start_device_id" => $start_device_id "start_device_id" => $start_device_id
]; ];
$db = new DbOperations(); $db = new DbOperations();
$db->insert("time_records", $data); $db->insert("time_records", $data);
$db->execute(); $db->execute();
} }
function getTimeRecord($user_id, $record_id) function getTimeRecord($user_id, $record_id)
{ {
$db = new DbOperations(); $db = new DbOperations();
$db->select("time_records"); $db->select("time_records");
$db->where("user_id", Comparison::EQUAL, $user_id); $db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("record_id", Comparison::EQUAL, $record_id); $db->where("record_id", Comparison::EQUAL, $record_id);
$result = $db->execute(); $result = $db->execute();
if (count($result) <= 0) { if (count($result) <= 0) {
return null; return null;
} }
$result = $result[0]; $result = $result[0];
// Is still running? // Is still running?
if ($result["end_time"] == null) { if ($result["end_time"] == null) {
$result["duration"] = calcDuration($result["start_time"]); $result["duration"] = calcDuration($result["start_time"]);
} }
return $result; return $result;
} }
function getProjectRecord($user_id, $project_id, $finished = null) function getProjects($user_id)
{ {
$db = new DbOperations(); $db = new DbOperations();
$db->select("time_records"); $db->select("projects");
$db->where("user_id", Comparison::EQUAL, $user_id); $db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("project_id", Comparison::EQUAL, $project_id); $results = $db->execute();
if ($finished != null) { foreach ($results as $key => $project) {
$comp = Comparison::UNEQUAL; $meta = getProjectRecordDerivedData($user_id, $project["project_id"]);
if ($finished == false) {
$comp = Comparison::EQUAL; foreach ($meta as $metaKey => $value) {
} $results[$key][$metaKey] = $value;
}
$db->where("end_time", $comp, null); }
}
return $results;
$db->orderBy("start_time", Order::DESC); }
$result = $db->execute();
function getProjectRecordDerivedData($user_id, $project_id)
if (count($result) <= 0) { {
return null; $durationAttribute = "SUM(duration) AS total_duration";
} $recordCountAttribute = "COUNT(*) AS record_count";
$result = $result[0];
$db = new DbOperations();
// Is still running? $db->select("time_records", ["*", $durationAttribute, $recordCountAttribute]);
if ($result["end_time"] == null) { $db->where("user_id", Comparison::EQUAL, $user_id);
$result["duration"] = calcDuration($result["start_time"]); $db->where("project_id", Comparison::EQUAL, $project_id);
} $results = $db->execute();
return $result; if (count($results) <= 0) {
} return ["duration" => 0, "record_count" => 0];
} else {
function updateEndRecord($user_id, $params) return [
{ "duration" => (int)$results[0]["total_duration"],
$record_id = $params->get("record_id"); "record_count" => (int)$results[0]["record_count"]
];
// Get start instance to calculate duration }
$start_time = getTimeRecord($user_id, $record_id)[0]["start_time"]; }
$data = [ function getProjectRecord($user_id, $project_id, $finished = null)
"end_time" => $params->get("end_time"), {
"duration" => calcDuration($start_time, $params->get("end_time")) $db = new DbOperations();
]; $db->select("time_records");
$db->where("user_id", Comparison::EQUAL, $user_id);
$db = new DbOperations(); $db->where("project_id", Comparison::EQUAL, $project_id);
$db->update("time_records", $data);
$db->where("user_id", Comparison::EQUAL, $user_id); if ($finished != null) {
$db->where("record_id", Comparison::EQUAL, $record_id); $comp = Comparison::UNEQUAL;
$db->execute(); if ($finished == false) {
} $comp = Comparison::EQUAL;
}
function updateTimeRecord($user_id, $params)
{ $db->where("end_time", $comp, null);
$data = []; }
$db->orderBy("start_time", Order::DESC);
$anythingUpdated = false; $result = $db->execute();
if ($params->exists(["start_time"])) {
$data["start_time"] = $params->get("start_time"); if (count($result) <= 0) {
$anythingUpdated = true; return null;
} }
if ($params->exists(["end_time"])) { $result = $result[0];
$data["end_time"] = $params->get("end_time");
$anythingUpdated = true; // Is still running?
} if ($result["end_time"] == null) {
if ($params->exists(["duration"])) { $result["duration"] = calcDuration($result["start_time"]);
$data["duration"] = $params->get("duration"); }
$anythingUpdated = true;
} return $result;
if ($params->exists(["project_id"])) { }
$data["project_id"] = $params->get("project_id");
$anythingUpdated = true; function updateEndRecord($user_id, $params)
} {
if ($params->exists(["start_device_id"])) { $record_id = $params->get("record_id");
$data["start_device_id"] = $params->get("start_device_id");
$anythingUpdated = true; // Get start instance to calculate duration
} $start_time = getTimeRecord($user_id, $record_id)[0]["start_time"];
if ($anythingUpdated == false) { $data = [
return; "end_time" => $params->get("end_time"),
} "duration" => calcDuration($start_time, $params->get("end_time"))
];
$db = new DbOperations();
$db->update("time_records", $data); $db = new DbOperations();
$db->where("user_id", Comparison::EQUAL, $user_id); $db->update("time_records", $data);
$db->where("record_id", Comparison::EQUAL, $params->get("record_id")); $db->where("user_id", Comparison::EQUAL, $user_id);
$db->execute(); $db->where("record_id", Comparison::EQUAL, $record_id);
} $db->execute();
}
function isProjectValid($project_id, $user_id)
{ function updateTimeRecord($user_id, $params)
$db = new DbOperations(); {
$db->select("projects"); $data = [];
$db->where("project_id", Comparison::EQUAL, $project_id);
$db->where("user_id", Comparison::EQUAL, $user_id);
$anythingUpdated = false;
return count($db->execute()) == 1; if ($params->exists(["start_time"])) {
} $data["start_time"] = $params->get("start_time");
$anythingUpdated = true;
function isDeviceValid($start_device_id, $user_id) }
{ if ($params->exists(["end_time"])) {
$db = new DbOperations(); $data["end_time"] = $params->get("end_time");
$db->select("devices"); $anythingUpdated = true;
$db->where("start_device_id", Comparison::EQUAL, $start_device_id); }
$db->where("user_id", Comparison::EQUAL, $user_id); if ($params->exists(["duration"])) {
$data["duration"] = $params->get("duration");
return count($db->execute()) == 1; $anythingUpdated = true;
} }
if ($params->exists(["project_id"])) {
function calcDuration($start_time, $end_time = "NOW") $data["project_id"] = $params->get("project_id");
{ $anythingUpdated = true;
return (int)((new DateTime($start_time))->diff(new DateTime($end_time))->format("%s")); }
} if ($params->exists(["start_device_id"])) {
$data["start_device_id"] = $params->get("start_device_id");
$anythingUpdated = true;
}
if ($anythingUpdated == false) {
return;
}
$db = new DbOperations();
$db->update("time_records", $data);
$db->where("user_id", Comparison::EQUAL, $user_id);
$db->where("record_id", Comparison::EQUAL, $params->get("record_id"));
$db->execute();
}
function isProjectValid($project_id, $user_id)
{
$db = new DbOperations();
$db->select("projects");
$db->where("project_id", Comparison::EQUAL, $project_id);
$db->where("user_id", Comparison::EQUAL, $user_id);
return count($db->execute()) == 1;
}
function isDeviceValid($start_device_id, $user_id)
{
$db = new DbOperations();
$db->select("devices");
$db->where("start_device_id", Comparison::EQUAL, $start_device_id);
$db->where("user_id", Comparison::EQUAL, $user_id);
return count($db->execute()) == 1;
}
function calcDuration($start_time, $end_time = "NOW")
{
return (int)((new DateTime($start_time))->diff(new DateTime($end_time))->format("%s"));
}

View file

@ -1,50 +1,50 @@
<?php <?php
class ParamCleaner { class ParamCleaner {
function __construct (array $params) { function __construct (array $params) {
$this->sourceParams = $params; $this->sourceParams = $params;
$this->selectedParams = $params; $this->selectedParams = $params;
$this->errorCount = 0; $this->errorCount = 0;
$this->errorMessage = ""; $this->errorMessage = "";
} }
function select (string $prop = "") { function select (string $prop = "") {
if ($prop == "") { if ($prop == "") {
$this->selectedParams = $this->sourceParams; $this->selectedParams = $this->sourceParams;
} else { } else {
$this->selectedParams = $this->selectedParams[$prop]; $this->selectedParams = $this->selectedParams[$prop];
} }
} }
function get (string $prop) { function get (string $prop) {
if(isset($this->selectedParams[$prop])) { if(isset($this->selectedParams[$prop])) {
return $this->selectedParams[$prop]; return $this->selectedParams[$prop];
} else { } else {
$this->errorCount += 1; $this->errorCount += 1;
$this->errorMessage .= "Property \"{$prop}\" missing. "; $this->errorMessage .= "Property \"{$prop}\" missing. ";
return null; return null;
} }
} }
function exists (array $props) { function exists (array $props) {
foreach ($props as $prop) { foreach ($props as $prop) {
if(isset($this->selectedParams[$prop]) == false) { if(isset($this->selectedParams[$prop]) == false) {
return false; return false;
} }
} }
return true; return true;
} }
function hasErrorOccurred () { function hasErrorOccurred () {
return $this->errorCount > 0; return $this->errorCount > 0;
} }
function getErrorMessage () { function getErrorMessage () {
return $this->errorMessage; return $this->errorMessage;
} }
function resetErrors () { function resetErrors () {
$this->errorMessage = ""; $this->errorMessage = "";
$this->errorCount = 0; $this->errorCount = 0;
} }
} }
?> ?>

View file

@ -1,19 +1,19 @@
<?php <?php
require_once(__DIR__.'/basicEnum.inc.php'); require_once(__DIR__.'/basicEnum.inc.php');
abstract class RequestType extends BasicEnum { abstract class RequestType extends BasicEnum {
const GET = "GET"; const GET = "GET";
const POST = "POST"; const POST = "POST";
const PUT = "PUT"; const PUT = "PUT";
const DELETE = "DELETE"; const DELETE = "DELETE";
} }
function currentRequestType () { function currentRequestType () {
$requestType = $_SERVER['REQUEST_METHOD']; $requestType = $_SERVER['REQUEST_METHOD'];
if (RequestType::isValidValue($requestType)) { if (RequestType::isValidValue($requestType)) {
return $requestType; return $requestType;
} else { } else {
return null; return null;
} }
} }
?> ?>

View file

@ -1,25 +1,25 @@
<?php <?php
require_once(__DIR__."/jsonBuilder.inc.php"); require_once(__DIR__."/jsonBuilder.inc.php");
function respondJson(JsonBuilder $builder) { function respondJson(JsonBuilder $builder) {
header('Content-type: application/json'); header('Content-type: application/json');
echo($builder->getJson()); echo($builder->getJson());
} }
function respondHtml(string $html) { function respondHtml(string $html) {
print($html); print($html);
} }
function respondStatus(int $statusCode, string $message = "") { function respondStatus(int $statusCode, string $message = "") {
http_response_code($statusCode); http_response_code($statusCode);
die($message); die($message);
} }
function redirectBack() { function redirectBack() {
header("Location: {$_SERVER['HTTP_REFERER']}"); header("Location: {$_SERVER['HTTP_REFERER']}");
} }
function redirectTo($url) { function redirectTo($url) {
header("Location: {$url}"); header("Location: {$url}");
} }
?> ?>

View file

@ -1,50 +1,49 @@
<?php <?php
session_start(); session_start();
require_once(__DIR__ . "/services/apiBranch.inc.php"); require_once(__DIR__ . "/services/apiBranch.inc.php");
require_once(__DIR__ . "/services/jsonBuilder.inc.php"); require_once(__DIR__ . "/services/jsonBuilder.inc.php");
require_once(__DIR__ . "/services/responses.inc.php"); require_once(__DIR__ . "/services/responses.inc.php");
require_once(__DIR__ . "/services/jugglDbApi.inc.php"); require_once(__DIR__ . "/services/jugglDbApi.inc.php");
class StartRecordBranch extends ApiBranch class StartRecordBranch extends ApiBranch
{ {
function get(ParamCleaner $params) function get(ParamCleaner $params)
{ {
respondStatus(405); respondStatus(405);
} }
function post(ParamCleaner $params) function post(ParamCleaner $params)
{ {
$user_id = $params->get("user_id"); $user_id = $params->get("user_id");
$params->select("request");
if ($params->exists(["start_time"]) == false) {
if ($params->exists(["start_time"]) == false) { respondStatus(400, "Missing parameter");
respondStatus(400, "Missing parameter"); }
}
$project_id = $params->get("project_id");
$project_id = $params->get("project_id"); if (isProjectValid($project_id, $user_id) == false) {
if (isProjectValid($project_id, $user_id) == false) { $project_id = null;
$project_id = null; }
}
$device_id = $params->get("start_device_id");
$device_id = $params->get("start_device_id"); if (isDeviceValid($device_id, $user_id) == false) {
if (isDeviceValid($device_id, $user_id) == false) { $device_id = null;
$device_id = null; }
}
// Does a running record for that project already exist?
// Does a running record for that project already exist? if (getProjectRecord($user_id, $project_id, false) != null) {
if (getProjectRecord($user_id, $project_id, false) != null) { respondStatus(409, "Project record already started");
respondStatus(409, "Project record already started"); }
}
addStartRecord($user_id, $params, $project_id, $device_id);
addStartRecord($user_id, $params, $project_id, $device_id); $record = getProjectRecord($user_id, $project_id, false);
$record = getProjectRecord($user_id, $project_id, false);
$json = new JsonBuilder();
$json = new JsonBuilder(); $json->addRecords([$record]);
$json->addRecords([$record]);
respondJson($json);
respondJson($json); }
} }
}
$branch = new StartRecordBranch();
$branch = new StartRecordBranch(); $branch->execute();
$branch->execute();

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View file

@ -0,0 +1,91 @@
body {
margin: 0;
background-color: #222;
}
* {
color: white;
font-family: 'Karla', sans-serif;
transition-duration: 0.1s;
}
.hidden {
display: none;
}
header {
width: 100%;
height: 4rem;
position: fixed;
top: 0px;
background-color: #222;
/* background: rgb(10, 133, 168);
background: linear-gradient(137deg, rgba(10, 133, 168, 1) 0%, rgba(136, 0, 0, 1) 100%); */
/* background: rgb(10, 133, 168);
background: linear-gradient(137deg, rgba(10, 133, 168, 1) 0%, rgba(255, 255, 255, 1) 48%, rgba(136, 0, 0, 1) 100%); */
/* offset-x | offset-y | blur-radius | spread-radius | color */
box-shadow: 0px 0px 2rem 1rem #000D;
}
main {
max-width: 800px;
padding: 0px 20px;
margin: auto;
margin-top: 6rem;
background-color: #0000;
}
#header-container {
max-width: 1000px;
padding: 0px 20px;
margin: auto;
list-style: none;
}
#header-container img {
margin-top: 0.5rem;
height: 3rem;
width: auto;
}
#header-container>* {
line-height: 4rem;
vertical-align: baseline;
display: inline-block;
}
textarea:focus,
input:focus,
button:focus {
outline: none;
}
input,
button {
border-radius: 2px;
border: solid red 1px;
background-color: #0000;
color: #BBB;
padding: 5px;
font-size: 12pt;
}
button:hover {
color: white;
background-color: #F007;
}
button:active {
transform: translate(0px, 2px);
}
table {
border-collapse: collapse;
width: 100%;
}
thead * {
color: #AAA;
text-align: left;
}

61
juggl-server/index.html Normal file
View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<title>Juggl</title>
<meta charset="UTF-8" />
<link rel="shortcut icon" type="image/ico" href="/assets/logo.ico" />
<link href="https://fonts.googleapis.com/css2?family=Karla:ital,wght@0,400;0,700;1,400;1,700&display=swap"
rel="stylesheet">
<link href="css/style.css" rel="stylesheet">
<script src="js/umbrella.min.js"></script>
<script src="js/visibility.js"></script>
<script src="js/auth.js"></script>
<script src="js/api.js"></script>
<script src="js/helper.js"></script>
<script>
window.onload = () => {
// Create Callbacks
callbacks.leaving[States.PUBLIC] = () => {
loadProjectList();
};
initState();
updateVisibility();
updateAuthBtnText();
}
</script>
</head>
<body>
<header>
<ul id="header-container">
<li>
<img src="assets/logo_title.png">
</li>
<li style="float: right">
<input id="user-id" class="public" type="text" placeholder="User ID" />
<input id="api-key" class="public" type="password" placeholder="API Key" />
<button id="auth-btn" onclick="handleAuthBtn()">Log In</button>
</li>
</ul>
</header>
<main>
<table class="not-public hidden">
<thead>
<th>Project</th>
<th>Start date</th>
<th>Duration</th>
<th>Records</th>
<th>Average record</th>
</thead>
<tbody id="project-list">
</tbody>
</table>
</main>
</body>
</html>

32
juggl-server/js/api.js Normal file
View file

@ -0,0 +1,32 @@
API_URL = "/api";
const api = {
getProjects() {
return request("/getProjects.php")
.then((r) => {
return r.json();
})
.then((j) => {
return j.projects;
})
.catch((e) => {
console.log(e);
return [];
});
},
};
function request(path, json = {}, options = {}) {
json.api_key = getApiKey();
json.user_id = getUserId();
options.method = "POST";
options.body = JSON.stringify(json);
options.headers = {
"Content-Type": "application/json",
};
var url = API_URL + path;
return fetch(url, options);
}

58
juggl-server/js/auth.js Normal file
View file

@ -0,0 +1,58 @@
function logIn(apiKey, userId) {
localStorage.apiKey = apiKey;
localStorage.userId = userId;
setState(States.IDLE);
}
function logOut() {
delete localStorage.apiKey;
delete localStorage.userId;
setState(States.PUBLIC);
}
function isLoggedIn() {
return !!localStorage.apiKey && !!localStorage.userId;
}
function getApiKey() {
return localStorage.apiKey;
}
function getUserId() {
return localStorage.userId;
}
function handleAuthBtn() {
if (isLoggedIn()) {
logOut();
} else {
var apiKey = u("#api-key").first().value;
if (apiKey === undefined || apiKey === "") {
return;
}
var userId = u("#user-id").first().value;
if (userId === undefined || userId === "") {
return;
}
logIn(apiKey, userId);
onLogIn();
}
u("#api-key").first().value = "";
updateAuthBtnText();
}
function updateAuthBtnText() {
var btn = u("#auth-btn");
if (isLoggedIn()) {
btn.text("Log Out");
} else {
btn.text("Log In");
}
}
function onLogIn() {
loadProjectList();
}

59
juggl-server/js/helper.js Normal file
View file

@ -0,0 +1,59 @@
const TABLE_ROW = "tr";
const TABLE_DATA = "td";
function loadProjectList() {
api.getProjects().then((projects) => {
var table = u(u("#project-list").first());
Object.values(projects).forEach((project) => {
var row = createNode(TABLE_ROW);
var data = undefined;
data = createNode(TABLE_DATA);
append(row, data);
u(data).text(project["name"]);
data = createNode(TABLE_DATA);
append(row, data);
u(data).text(new Date(project["start_date"]).toDateString());
var duration = parseFloat(project["duration"]) / 60 / 60;
var unit = "hours";
data = createNode(TABLE_DATA);
append(row, data);
u(data).text(duration + " " + unit);
data = createNode(TABLE_DATA);
append(row, data);
u(data).text(project["record_count"]);
data = createNode(TABLE_DATA);
append(row, data);
u(data).text(
duration / parseInt(project["record_count"]) + " " + unit + "/record"
);
row = u(row);
row.data("project-id", project["project_id"]);
row.on("click", projectClicked);
table.append(row);
});
});
}
// Created new DOM object
// element: Type of DOM object (div, p, ...)
function createNode(element) {
return document.createElement(element);
}
// Appends child to parent
// parent: DOM to append child to
// el: DOM child to append to parent
function append(parent, el) {
return parent.appendChild(el);
}
function projectClicked(event) {
console.log(event);
}

3
juggl-server/js/umbrella.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,55 @@
/**
* Values represent classes.
*
* classname: Only visible in class-state.
* not-classname: Not visible in class-state.
*/
const States = {
PUBLIC: "public",
IDLE: "idle",
RECORDING: "recording",
SETUP: "setup",
};
Object.freeze(States);
let previousState = States.PUBLIC;
let currentState = States.PUBLIC;
let callbacks = {
leaving: {},
entering: {},
};
function updateVisibility() {
u("." + previousState).addClass("hidden");
u("." + currentState).removeClass("hidden");
u(".not-" + previousState).removeClass("hidden");
u(".not-" + currentState).addClass("hidden");
processCallbacks();
}
function processCallbacks() {
var cb = callbacks.leaving[previousState];
if (cb !== undefined) cb();
cb = callbacks.entering[currentState];
if (cb !== undefined) cb();
}
function setState(state) {
if (Object.values(States).includes(state) === false) {
return;
}
previousState = currentState;
currentState = state;
localStorage.state = state;
updateVisibility();
}
function initState() {
var savedState = localStorage.state;
if (savedState !== undefined) currentState = localStorage.state;
}

View file

@ -1,45 +1,45 @@
Juggl Documentation Juggl Documentation
Goal Goal
Hold information about projects Hold information about projects
Timetracking Timetracking
Invoice generation Invoice generation
Shared files Shared files
ToDo ToDo
Progress Progress
Shared Workspace for client Shared Workspace for client
Deadlines Deadlines
API interface API interface
Ticket system Ticket system
ToDo ToDo
High Priority High Priority
Tracking Windows Client Tracking Windows Client
Refactor TimeLogger Refactor TimeLogger
Offline BackUps Offline BackUps
Nur tracking wenn aktiviert Nur tracking wenn aktiviert
Time tracking Time tracking
API interface API interface
Web interface Web interface
General General
Hold some kind of current state Hold some kind of current state
e.g. "Currently working on" e.g. "Currently working on"
Low Priority Low Priority
Web interface Web interface
Datbase Datbase
Table prefix Table prefix
ju_ ju_
API API
timetracking timetracking
POST POST
api_key api_key
user_id user_id
request request
start_time (required for insert) start_time (required for insert)
end_time (required for insert) end_time (required for insert)
duration (required for insert) duration (required for insert)
project_id (optional) project_id (optional)
device_id (optional) device_id (optional)
record_id (optional, update when given) record_id (optional, update when given)