Skip to content

Commit

Permalink
Wire JSON storage into the API
Browse files Browse the repository at this point in the history
  • Loading branch information
cpeel committed Dec 22, 2024
1 parent 33f354c commit 974c9b6
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 0 deletions.
21 changes: 21 additions & 0 deletions SETUP/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,24 @@ Three settings in `configuration.sh` control limiting:
allowed per given window.
* `_API_RATE_LIMIT_SECONDS_IN_WINDOW` - the number of seconds within a given
window.

## Client Storage

To facilitate javascript UI clients persisting data across browsers and devices,
the API includes an optional endpoint for clients to store and fetch JSON blobs.
To enable this feature, add a string for the client to the
`_API_CLIENT_STORAGE_KEYS` configuration setting and have the client use that
string with the endpoint as the `clientid`.

Some important notes about this feature:
* Client storage is one blob per user per client. Said another way: API users are
only able to store one blob per `clientid` and that blob is only for the user
authenticated with the API.
* Beyond validating these are valid JSON objects, they are treated as opaque
blobs server-side. It is up to the client to manage the object, including
the schema and the possibility that the object will not match an expected
schema.
* The `clientid` is not a secret to the browser. Nothing prevents users with
API keys (or valid PHP session keys) from using this endpoint with a valid
client ID to change the contents of this blob for their user. Clients
should treat this blob as unvalidated user input and act accordingly.
45 changes: 45 additions & 0 deletions SETUP/tests/unittests/ApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,9 @@ public function test_pickersets(): void
$this->assertEquals(["¿", "INVERTED QUESTION MARK"], $pickerset["subsets"][3]["rows"][1][1]);
}

//---------------------------------------------------------------------------
// tests for documents

public function test_available_italian_documents(): void
{
$path = "v1/documents";
Expand Down Expand Up @@ -823,6 +826,48 @@ public function test_unavailable_document(): void
$_SERVER["REQUEST_METHOD"] = "GET";
$router->route($path, ['language_code' => 'de']);
}

//---------------------------------------------------------------------------
// tests for storage

public function test_client_storage_valid(): void
{
global $pguser;
global $api_client_storage_keys;
global $request_body;

$pguser = $this->TEST_USERNAME_PM;
array_push($api_client_storage_keys, "valid");

$path = "v1/storage/clients/valid";
$query_params = [];
$request_body = ["key" => 1];
$router = ApiRouter::get_router();

$_SERVER["REQUEST_METHOD"] = "PUT";
$response = $router->route($path, $query_params);
$this->assertEquals($request_body, (array)$response);

$_SERVER["REQUEST_METHOD"] = "GET";
$response = $router->route($path, $query_params);
$this->assertEquals($request_body, (array)$response);

$_SERVER["REQUEST_METHOD"] = "DELETE";
$response = $router->route($path, $query_params);
$this->assertEquals(null, $response);
}

public function test_client_storage_invalid(): void
{
$this->expectExceptionCode(4);

$query_params = [];

$path = "v1/storage/clients/invalid";
$_SERVER["REQUEST_METHOD"] = "GET";
$router = ApiRouter::get_router();
$router->route($path, $query_params);
}
}

// this mocks the function in index.php
Expand Down
52 changes: 52 additions & 0 deletions api/dp-openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,58 @@ paths:
404:
$ref: '#/components/responses/NotFound'

/storage/clients/{clientid}:
get:
tags:
- storage
description: Get JSON blob stored by the client
parameters:
- name: clientid
in: path
description: Client ID
required: true
schema:
type: string
responses:
200:
description: JSON blob
content:
application/json:
schema:
type: object
put:
tags:
- storage
description: Save JSON blob for the client
parameters:
- name: clientid
in: path
description: Client ID
required: true
schema:
type: string
responses:
200:
description: JSON blob that was persisted
content:
application/json:
schema:
type: object
delete:
tags:
- storage
description: Delete JSON blob for the client
parameters:
- name: clientid
in: path
description: Client ID
required: true
schema:
type: string
responses:
200:
description: JSON blob was deleted

components:
securitySchemes:
ApiKeyAuth:
Expand Down
6 changes: 6 additions & 0 deletions api/v1.inc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ include_once("v1_projects.inc");
include_once("v1_queues.inc");
include_once("v1_stats.inc");
include_once("v1_docs.inc");
include_once("v1_storage.inc");

$router = ApiRouter::get_router();

Expand All @@ -18,6 +19,7 @@ $router->add_validator(":pagename", "validate_page_name");
$router->add_validator(":pageroundid", "validate_page_round");
$router->add_validator(":queueid", "validate_release_queue");
$router->add_validator(":document", "validate_document");
$router->add_validator(":clientid", "validate_client_id");

// Add routes
$router->add_route("GET", "v1/documents", "api_v1_documents");
Expand Down Expand Up @@ -62,3 +64,7 @@ $router->add_route("GET", "v1/stats/site/projects/stages", "api_v1_stats_site_pr
$router->add_route("GET", "v1/stats/site/projects/states", "api_v1_stats_site_projects_states");
$router->add_route("GET", "v1/stats/site/rounds", "api_v1_stats_site_rounds");
$router->add_route("GET", "v1/stats/site/rounds/:roundid", "api_v1_stats_site_round");

$router->add_route("GET", "v1/storage/clients/:clientid", "api_v1_storage_clients");
$router->add_route("PUT", "v1/storage/clients/:clientid", "api_v1_storage_clients");
$router->add_route("DELETE", "v1/storage/clients/:clientid", "api_v1_storage_clients_delete");
32 changes: 32 additions & 0 deletions api/v1_storage.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

// DP API v1 -- Storage

//===========================================================================
// storage/clients/:clientid

/** @return mixed */
function api_v1_storage_clients(string $method, array $data, array $query_params)
{
global $pguser;

$clientid = $data[":clientid"];

$storage = new ApiClientStorage($clientid, $pguser);
if ($method == "GET") {
return json_decode($storage->get());
} elseif ($method == "PUT") {
$storage->set(json_encode(api_get_request_body()));
return json_decode($storage->get());
}
}

function api_v1_storage_clients_delete(string $method, array $data, array $query_params): void
{
global $pguser;

$clientid = $data[":clientid"];

$storage = new ApiClientStorage($clientid, $pguser);
$storage->delete();
}
10 changes: 10 additions & 0 deletions api/v1_validators.inc
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,13 @@ function validate_document(string $document): string
}
return $document;
}

function validate_client_id(string $clientid, array $data): string
{
global $api_client_storage_keys;

if (!in_array($clientid, $api_client_storage_keys)) {
throw new NotFoundError("$clientid is not a valid client id");
}
return $clientid;
}

0 comments on commit 974c9b6

Please sign in to comment.