diff --git a/.env b/.env index e156a066..5f105b59 100755 --- a/.env +++ b/.env @@ -28,4 +28,4 @@ ELASTIC_AUTH=ember-nexus-elasticsearch:9200 REDIS_AUTH=tcp://ember-nexus-redis?password=redis-password RABBITMQ_AUTH=amqp://user:password@ember-nexus-rabbitmq:5672 -REFERENCE_DATASET_VERSION=0.0.6 +REFERENCE_DATASET_VERSION=0.0.9 diff --git a/.markdownlintrc b/.markdownlintrc index 2ecbea0b..03b27506 100755 --- a/.markdownlintrc +++ b/.markdownlintrc @@ -2,5 +2,6 @@ "default": true, "MD013": false, "MD033": false, + "MD038": false, "MD041": false } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f4912cc..70000529 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Added +- Add feature test for GET `/token` endpoint +- Add feature test for POST `/token` endpoint +- Add feature test for DELETE `/token` endpoint ### Changed +- Increase reference dataset version to 0.0.8. - **Switch license to GPL-3.0-only.** ## 0.0.27 - 2023-09-02 diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 5ccd647a..faa1c201 100755 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -14,25 +14,47 @@ - [Passwords, Tokens and Hashing](/security/passwords-tokens-and-hashing) - [Security Tests](/security/test/general) - API Endpoints - - Generic Endpoints - - [`GET / -` Get Index](/api-endpoints/get-index) - - [`GET / -` Get Element](/api-endpoints/get-element) - - [`GET //parents -` Get Parents](/api-endpoints/get-parents) - - [`GET //children -` Get Children](/api-endpoints/get-children) - - [`GET //related -` Get Related](/api-endpoints/get-related) - - [`POST / -` Create Root Element](/api-endpoints/post-index) - - [`POST / -` Create Element](/api-endpoints/post-element) - - [`PUT / -` Replace Element](/api-endpoints/put-element) - - [`PATCH / -` Update Element](/api-endpoints/patch-element) - - [`DELETE / -` Delete Element](/api-endpoints/delete-element) - - [`POST /search -` Search](/api-endpoints/post-search) - - [`GET /instance-configuration -` Get Instance Configuration](/api-endpoints/get-instance-configuration) - - User Endpoints - - [`POST /register -` Register New Account](/api-endpoints/post-register) - - [`POST /sessions -` Create Session](/api-endpoints/post-sessions) - - [`GET /sessions -` Get Sessions](/api-endpoints/get-sessions) - - [`GET /sessions/ -` Get Specific Session](/api-endpoints/get-specific-session) - - [`DELETE /sessions/ -` Delete Session](/api-endpoints/delete-session) + + - **User Endpoints** + - [POST` /register -` Register New Account](/api-endpoints/user/post-register) + - [POST` /token -` Create Token](/api-endpoints/user/post-token) + - [GET` /token -` Get Token](/api-endpoints/user/get-token) + - [DELETE` /token -` Delete Token](/api-endpoints/user/delete-token) + - **Element Endpoints** + - [GET` / -` Get Index](/api-endpoints/element/get-index) + - [GET` / -` Get Element](/api-endpoints/element/get-element) + - [GET` //parents -` Get Parents](/api-endpoints/element/get-parents) + - [GET` //children -` Get Children](/api-endpoints/element/get-children) + - [GET` //related -` Get Related](/api-endpoints/element/get-related) + - [POST` / -` Create Root Element](/api-endpoints/element/post-index) + - [POST` / -` Create Element](/api-endpoints/element/post-element) + - [PUT` / -` Replace Element](/api-endpoints/element/put-element) + - [PATCH` / -` Update Element](/api-endpoints/element/patch-element) + - [DELETE` / -` Delete Element](/api-endpoints/element/delete-element) + - **File Endpoints** + - [🚧 GET` //file -` Get Element File](/api-endpoints/file/get-element-file) + - [🚧 POST` //file -` Create Element File](/api-endpoints/file/post-element-file) + - [🚧 PUT` //file -` Replace Element File](/api-endpoints/file/put-element-file) + - [🚧 PATCH` //file -` Update Element File](/api-endpoints/file/patch-element-file) + - [🚧 DELETE` //file -` Delete Element File](/api-endpoints/file/delete-element-file) + - **WebDAV Endpoints** + - [🚧 COPY` / -` Copy Element](/api-endpoints/webdav/copy-element) + - [🚧 LOCK` / -` Lock Element](/api-endpoints/webdav/lock-element) + - [🚧 UNLOCK` / -` Unlock Element](/api-endpoints/webdav/unlock-element) + - [🚧 MKCOL` / -` Create Folder](/api-endpoints/webdav/mkcol-folder) + - [🚧 MOVE` / -` Move Element](/api-endpoints/webdav/move-element) + - [🚧 PROPFIND` / -` Find Element Property](/api-endpoints/webdav/propfind-element) + - [🚧 PROPPATCH` / -` Change Element Property](/api-endpoints/webdav/proppatch-element) + - **Search Endpoints** + - [POST` /search -` Search](/api-endpoints/search/post-search) + - **System Endpoints** + - [GET` /instance-configuration -` Get Instance Configuration](/api-endpoints/system/get-instance-configuration) + - **Error Endpoints** + - [GET` /error/400/bad-content`](/api-endpoints/error/get-400-bad-content) + - [GET` /error/400/forbidden-property`](/api-endpoints/error/get-400-forbidden-property) + - [GET` /error/400/incomplete-mutual-dependency`](/api-endpoints/error/get-400-incomplete-mutual-dependency) + - [GET` /error/400/missing-property`](/api-endpoints/error/get-400-missing-property) + - Commands - [Backup Commands](/commands/backup) - [Database Commands](/commands/database) diff --git a/docs/api-endpoints/delete-element.md b/docs/api-endpoints/delete-element.md deleted file mode 100644 index 8a7dbc1d..00000000 --- a/docs/api-endpoints/delete-element.md +++ /dev/null @@ -1,5 +0,0 @@ -# DELETE /<uuid> - Delete Element - -Deletes a single element. If the delted element is a node, all connected relationships are deleted. - -Note: In order to avoid orphaned nodes, children need to be deleted first or get other parents added. diff --git a/docs/api-endpoints/delete-session.md b/docs/api-endpoints/delete-session.md deleted file mode 100644 index d471c8b9..00000000 --- a/docs/api-endpoints/delete-session.md +++ /dev/null @@ -1,6 +0,0 @@ -# DELETE /sessions/<uuid> - Delete Session - -Endpoint for deleting specific sessions. The uuid can be replaced by the following values: - -- `me`: Deletes all sessions of the current user. -- `current`: Deletes the current session. diff --git a/docs/api-endpoints/element/delete-element.md b/docs/api-endpoints/element/delete-element.md new file mode 100644 index 00000000..1e18a8fa --- /dev/null +++ b/docs/api-endpoints/element/delete-element.md @@ -0,0 +1,151 @@ +# DELETE` / -` Delete Element + + + + +Deletes a single element. If the deleted element is a node, all connected relationships are deleted. + +!> **Note**: In order to avoid orphaned nodes, children need to be deleted first or get other parents added. +This behaviour might be changed, see issue [#64: HTTP DELETE /<uuid> - DeleteElementController](https://github.com/ember-nexus/api/issues/64). + +## Request Example + +```bash +curl \ + -X DELETE + -H "Authorization: Bearer secret-token:PIPeJGUt7c00ENn8a5uDlc" \ + https://api.localhost/2f99440e-ca4c-4e83-bf86-1cd27a4b1b70 +``` + + + +### **Success 204** + +The element is now deleted. No content is returned. + +### **Error 401** + +This error can only be thrown, if the token is invalid or if there is no default anonymous user. + +```problem+json +{ + "type": "Invalid authorization token", + "title": "Unauthorized", + "status": "401", + "detail": "Request requires authorization." +} +``` + +### **Error 404** + +Error 404 is thrown if the element to be deleted does not exist, or if the use does not have permissions to delete the +element. + +```problem+json +{ + "type": "Invalid authorization token", + "title": "wip", + "status": "404", + "detail": "wip" +} +``` + +### **Error 429** + +```problem+json +{ + "type": "429-too-many-requests", + "title": "Too Many Requests", + "status": "429", + "detail": "The client sent too many requests in a given timeframe; rate limiting is active." +} +``` + + + + + +## Internal Workflow + +Once the server receives such a request, it checks several things internally: + +
+ + + + diff --git a/docs/api-endpoints/get-children.md b/docs/api-endpoints/element/get-children.md similarity index 71% rename from docs/api-endpoints/get-children.md rename to docs/api-endpoints/element/get-children.md index b5599ff2..44ce1ee6 100644 --- a/docs/api-endpoints/get-children.md +++ b/docs/api-endpoints/element/get-children.md @@ -1,4 +1,4 @@ -# GET /<uuid>/children - Get Children +# GET` //children -` Get Children Returns all children of the specified node. Returned data is paginated, can be filtered/sorted (?) and each page contains all relations between the parent and the diff --git a/docs/api-endpoints/get-element.md b/docs/api-endpoints/element/get-element.md similarity index 98% rename from docs/api-endpoints/get-element.md rename to docs/api-endpoints/element/get-element.md index 4a974fba..1ce5c4d9 100644 --- a/docs/api-endpoints/get-element.md +++ b/docs/api-endpoints/element/get-element.md @@ -1,4 +1,4 @@ -# GET /<uuid> - Get Element +# GET` / -` Get Element diff --git a/docs/api-endpoints/get-index.md b/docs/api-endpoints/element/get-index.md similarity index 98% rename from docs/api-endpoints/get-index.md rename to docs/api-endpoints/element/get-index.md index 2bb5dfa8..4f0af729 100644 --- a/docs/api-endpoints/get-index.md +++ b/docs/api-endpoints/element/get-index.md @@ -1,4 +1,4 @@ -# GET / - Get Index +# GET` / -` Get Index diff --git a/docs/api-endpoints/get-parents.md b/docs/api-endpoints/element/get-parents.md similarity index 79% rename from docs/api-endpoints/get-parents.md rename to docs/api-endpoints/element/get-parents.md index 7d51e8a2..e00995c6 100644 --- a/docs/api-endpoints/get-parents.md +++ b/docs/api-endpoints/element/get-parents.md @@ -1,4 +1,4 @@ -# GET /<uuid>/parents - Get Parents +# GET` //parents -` Get Parents Returns all parents of the specified node. Returned data is paginated, can be filtered/sorted (?) and each page contains all relations between the node and the diff --git a/docs/api-endpoints/get-related.md b/docs/api-endpoints/element/get-related.md similarity index 75% rename from docs/api-endpoints/get-related.md rename to docs/api-endpoints/element/get-related.md index 0c1b4936..34cf24c0 100644 --- a/docs/api-endpoints/get-related.md +++ b/docs/api-endpoints/element/get-related.md @@ -1,4 +1,4 @@ -# GET /<uuid>/related - Get Related +# GET` //related -` Get Related Returns all nodes related to the current node. Returned data is paginated, can be filtered/sorted (?) and each page contains all relations between the node and the diff --git a/docs/api-endpoints/patch-element.md b/docs/api-endpoints/element/patch-element.md similarity index 67% rename from docs/api-endpoints/patch-element.md rename to docs/api-endpoints/element/patch-element.md index 45cb85ad..23558e45 100644 --- a/docs/api-endpoints/patch-element.md +++ b/docs/api-endpoints/element/patch-element.md @@ -1,4 +1,4 @@ -# PATCH /<uuid> - Update Element +# PATCH` / -` Update Element Updates an individual data element. diff --git a/docs/api-endpoints/element/post-element.md b/docs/api-endpoints/element/post-element.md new file mode 100644 index 00000000..80313793 --- /dev/null +++ b/docs/api-endpoints/element/post-element.md @@ -0,0 +1,3 @@ +# POST` / -` Create Element + +Creates a new data element. It is owned by the referenced node. diff --git a/docs/api-endpoints/post-index.md b/docs/api-endpoints/element/post-index.md similarity index 60% rename from docs/api-endpoints/post-index.md rename to docs/api-endpoints/element/post-index.md index 93741adb..591f8a58 100644 --- a/docs/api-endpoints/post-index.md +++ b/docs/api-endpoints/element/post-index.md @@ -1,3 +1,3 @@ -# POST / - Create Root Element +# POST` / -` Create Root Element Creates a new data element. If the data element is a node, it is directly owned by the current user. diff --git a/docs/api-endpoints/put-element.md b/docs/api-endpoints/element/put-element.md similarity index 70% rename from docs/api-endpoints/put-element.md rename to docs/api-endpoints/element/put-element.md index 0c26c232..5d2b4a9f 100644 --- a/docs/api-endpoints/put-element.md +++ b/docs/api-endpoints/element/put-element.md @@ -1,4 +1,4 @@ -# PUT /<uuid> - Replace Element +# PUT` / -` Replace Element Replaces the data of an individual data element. diff --git a/docs/api-endpoints/error/get-400-bad-content.md b/docs/api-endpoints/error/get-400-bad-content.md new file mode 100644 index 00000000..e32d4db3 --- /dev/null +++ b/docs/api-endpoints/error/get-400-bad-content.md @@ -0,0 +1 @@ +# GET` /error/400/bad-content` diff --git a/docs/api-endpoints/error/get-400-forbidden-property.md b/docs/api-endpoints/error/get-400-forbidden-property.md new file mode 100644 index 00000000..a0c77c82 --- /dev/null +++ b/docs/api-endpoints/error/get-400-forbidden-property.md @@ -0,0 +1 @@ +# GET` /error/400/forbidden-property` diff --git a/docs/api-endpoints/error/get-400-incomplete-mutual-dependency.md b/docs/api-endpoints/error/get-400-incomplete-mutual-dependency.md new file mode 100644 index 00000000..f67eb397 --- /dev/null +++ b/docs/api-endpoints/error/get-400-incomplete-mutual-dependency.md @@ -0,0 +1 @@ +# GET` /error/400/incomplete-mutual-dependency` diff --git a/docs/api-endpoints/error/get-400-missing-property.md b/docs/api-endpoints/error/get-400-missing-property.md new file mode 100644 index 00000000..f7f5282d --- /dev/null +++ b/docs/api-endpoints/error/get-400-missing-property.md @@ -0,0 +1 @@ +# GET` /error/400/missing-property` diff --git a/docs/api-endpoints/file/delete-element-file.md b/docs/api-endpoints/file/delete-element-file.md new file mode 100644 index 00000000..b1f319d1 --- /dev/null +++ b/docs/api-endpoints/file/delete-element-file.md @@ -0,0 +1,4 @@ +# 🚧 DELETE` //file -` Delete Element File + +!> **Currently not implemented.** +This feature is reserved for the version [0.2.0](https://github.com/ember-nexus/api/milestone/1). diff --git a/docs/api-endpoints/file/get-element-file.md b/docs/api-endpoints/file/get-element-file.md new file mode 100644 index 00000000..8eaaa309 --- /dev/null +++ b/docs/api-endpoints/file/get-element-file.md @@ -0,0 +1,4 @@ +# 🚧 GET` //file -` Get Element File + +!> **Currently not implemented.** +This feature is reserved for the version [0.2.0](https://github.com/ember-nexus/api/milestone/1). diff --git a/docs/api-endpoints/file/patch-element-file.md b/docs/api-endpoints/file/patch-element-file.md new file mode 100644 index 00000000..ab883c1a --- /dev/null +++ b/docs/api-endpoints/file/patch-element-file.md @@ -0,0 +1,4 @@ +# 🚧 PATCH` //file -` Update Element File + +!> **Currently not implemented.** +This feature is reserved for the version [0.2.0](https://github.com/ember-nexus/api/milestone/1). diff --git a/docs/api-endpoints/file/post-element-file.md b/docs/api-endpoints/file/post-element-file.md new file mode 100644 index 00000000..24695903 --- /dev/null +++ b/docs/api-endpoints/file/post-element-file.md @@ -0,0 +1,4 @@ +# 🚧 POST` //file -` Create Element File + +!> **Currently not implemented.** +This feature is reserved for the version [0.2.0](https://github.com/ember-nexus/api/milestone/1). diff --git a/docs/api-endpoints/file/put-element-file.md b/docs/api-endpoints/file/put-element-file.md new file mode 100644 index 00000000..8b69f246 --- /dev/null +++ b/docs/api-endpoints/file/put-element-file.md @@ -0,0 +1,4 @@ +# 🚧 PUT` //file -` Replace Element File + +!> **Currently not implemented.** +This feature is reserved for the version [0.2.0](https://github.com/ember-nexus/api/milestone/1). diff --git a/docs/api-endpoints/get-sessions.md b/docs/api-endpoints/get-sessions.md deleted file mode 100644 index 426345b1..00000000 --- a/docs/api-endpoints/get-sessions.md +++ /dev/null @@ -1,3 +0,0 @@ -# GET /sessions - List Sessions - -Endpoint for retrieving the users current sessions. diff --git a/docs/api-endpoints/get-specific-session.md b/docs/api-endpoints/get-specific-session.md deleted file mode 100644 index 3d4cef95..00000000 --- a/docs/api-endpoints/get-specific-session.md +++ /dev/null @@ -1,3 +0,0 @@ -# GET /sessions/<uuid> - List Specific Session - -Endpoint for retrieving specific sessions of the user. diff --git a/docs/api-endpoints/patch-sessions.md b/docs/api-endpoints/patch-sessions.md deleted file mode 100644 index 1db29bd1..00000000 --- a/docs/api-endpoints/patch-sessions.md +++ /dev/null @@ -1,3 +0,0 @@ -# PATCH /sessions/<uuid> - Update Session - -Endpoint for updating sessions. diff --git a/docs/api-endpoints/post-element.md b/docs/api-endpoints/post-element.md deleted file mode 100644 index 0b9a798a..00000000 --- a/docs/api-endpoints/post-element.md +++ /dev/null @@ -1,3 +0,0 @@ -# POST / - Create Element - -Creates a new data element. It is owned by the referenced node. diff --git a/docs/api-endpoints/post-sessions.md b/docs/api-endpoints/post-sessions.md deleted file mode 100644 index 76b7c7ec..00000000 --- a/docs/api-endpoints/post-sessions.md +++ /dev/null @@ -1,4 +0,0 @@ -# POST /sessions - Create Session - -Endpoint for creating new sessions and API keys. A single session can be used indefinitely, or be restricted by global -limits. diff --git a/docs/api-endpoints/post-search.md b/docs/api-endpoints/search/post-search.md similarity index 97% rename from docs/api-endpoints/post-search.md rename to docs/api-endpoints/search/post-search.md index 7f119bc0..c0be183f 100644 --- a/docs/api-endpoints/post-search.md +++ b/docs/api-endpoints/search/post-search.md @@ -1,8 +1,11 @@ -# POST /search - Search +# POST` /search -` Search +!> **Note**: This endpoint might be changed during development to the +[0.3.0](https://github.com/ember-nexus/api/milestone/3) version. + The post search endpoint at `POST /search` is used to execute [Elasticsearch](https://www.elastic.co/) queries and return found elements. diff --git a/docs/api-endpoints/get-instance-configuration.md b/docs/api-endpoints/system/get-instance-configuration.md similarity index 96% rename from docs/api-endpoints/get-instance-configuration.md rename to docs/api-endpoints/system/get-instance-configuration.md index a78d0003..0ca2250c 100644 --- a/docs/api-endpoints/get-instance-configuration.md +++ b/docs/api-endpoints/system/get-instance-configuration.md @@ -1,4 +1,4 @@ -# GET /instance-configuration - Get Instance Configuration +# GET` /instance-configuration -` Get Instance Configuration diff --git a/docs/api-endpoints/user/delete-token.md b/docs/api-endpoints/user/delete-token.md new file mode 100644 index 00000000..ff17bf9c --- /dev/null +++ b/docs/api-endpoints/user/delete-token.md @@ -0,0 +1,3 @@ +# DELETE` /token -` Delete Token + +Deletes the currently used token. diff --git a/docs/api-endpoints/user/get-token.md b/docs/api-endpoints/user/get-token.md new file mode 100644 index 00000000..dc90d51e --- /dev/null +++ b/docs/api-endpoints/user/get-token.md @@ -0,0 +1,7 @@ +# GET` /token -` Get Token + +Returns the currently used token. + +To display all tokens, you can return all root elements and filter for the `Token` type. + +Currently under development. diff --git a/docs/api-endpoints/post-register.md b/docs/api-endpoints/user/post-register.md similarity index 87% rename from docs/api-endpoints/post-register.md rename to docs/api-endpoints/user/post-register.md index 618361b2..adbb1c15 100644 --- a/docs/api-endpoints/post-register.md +++ b/docs/api-endpoints/user/post-register.md @@ -1,4 +1,4 @@ -# POST /register - Register New Account +# POST` /register -` Register New Account @@ -14,7 +14,7 @@ The posted request must be a valid JSON document. The request must contain the following attributes: -- `type`: Containing the content "User". No other values are possible. +- `type`: Containing the content "User". No other values are currently possible. - `password`: The plain text password of the new user. Can contain any string, will be hashed internally. Whitespace at the start or end of the string will **not** be removed, though it is discouraged. No password complexity check is performed. @@ -133,6 +133,8 @@ renderWorkflow(document.getElementById('graph-container-1'), { { id: 'init', ...workflowStart, label: 'server receives POST-request' }, { id: 'checkEndpointEnabled', ...workflowDecision, label: 'is endpoint enabled?' }, { id: 'checkPassword', ...workflowDecision, label: 'is password given?' }, + { id: 'checkType', ...workflowDecision, label: 'is type given?' }, + { id: 'checkTypeContent', ...workflowDecision, label: 'is type equal to "User"?' }, { id: 'checkIdentifier', ...workflowDecision, label: "is identifier given?" }, { id: 'checkIdentifierUnique', ...workflowDecision, label: 'is identifier unique?' }, { id: 'createUser', ...workflowStep, label: "create user" }, @@ -144,8 +146,12 @@ renderWorkflow(document.getElementById('graph-container-1'), { { source: 'init', target: 'checkEndpointEnabled', label: '' }, { source: 'checkEndpointEnabled', target: 'checkPassword', label: 'yes' }, { source: 'checkEndpointEnabled', target: 'error403', label: 'no' }, - { source: 'checkPassword', target: 'checkIdentifier', label: 'yes' }, + { source: 'checkPassword', target: 'checkType', label: 'yes' }, { source: 'checkPassword', target: 'error400', label: 'no' }, + { source: 'checkType', target: 'checkTypeContent', label: 'yes' }, + { source: 'checkType', target: 'error400', label: 'no' }, + { source: 'checkTypeContent', target: 'checkIdentifier', label: 'yes' }, + { source: 'checkTypeContent', target: 'error400', label: 'no' }, { source: 'checkIdentifier', target: 'checkIdentifierUnique', label: 'yes' }, { source: 'checkIdentifier', target: 'error400', label: 'no' }, { source: 'checkIdentifierUnique', target: 'createUser', label: 'yes' }, diff --git a/docs/api-endpoints/user/post-token.md b/docs/api-endpoints/user/post-token.md new file mode 100644 index 00000000..9a5a5837 --- /dev/null +++ b/docs/api-endpoints/user/post-token.md @@ -0,0 +1,164 @@ +# POST` /token -` Create Token + + + + +Endpoint for creating new tokens. + +Endpoint can be configured, see +[application configuration](/getting-started/configuration?id=application-configuration) for details. + +## Request Body + +The posted request must be a valid JSON document. + +The request must contain the following attributes: + +- `type`: Containing the content "Token". No other values are currently possible. +- `user`: The value for the user's identifying property, by default the user's email address. +- `password`: The plain text password of the user. +- `data`: Object of properties, optional. + +```json +{ + "type": "Token", + "user": "test@localhost.dev", + "password": "1234", + "data": { + "key": "value" + } +} +``` + +## Request Example + +```bash +curl \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{"type": "Token", "user": "test@localhost.dev", "password": "1234"}' \ + https://api.localhost/token +``` + + + +### **Success 200** + +```json +{ + "type": "_TokenResponse", + "token": "secret-token:ERgAAnWl0CY8bQs0m11nZ3" +} +``` + +### **Error 400** + +```problem+json +{ + "type": "400-bad-request", + "title": "Bad Request", + "status": 400, + "detail": "Property 'user' must be set." +} +``` + +### **Error 401** + +```problem+json +{ + "type": "401-unauthorized", + "title": "Request does not contain valid token, or anonymous user is disabled.", + "status": 401, + "detail": "" +} +``` + +### **Error 500** + +wip + + + + + +## Internal Workflow + +Once the server receives such a request, it checks several things internally: + +
+ + + + diff --git a/docs/api-endpoints/webdav/copy-element.md b/docs/api-endpoints/webdav/copy-element.md new file mode 100644 index 00000000..037db92e --- /dev/null +++ b/docs/api-endpoints/webdav/copy-element.md @@ -0,0 +1,4 @@ +# 🚧 COPY` / -` Copy Element + +!> **Currently not implemented.** +This feature is reserved for the version [0.2.0](https://github.com/ember-nexus/api/milestone/1). diff --git a/docs/api-endpoints/webdav/lock-element.md b/docs/api-endpoints/webdav/lock-element.md new file mode 100644 index 00000000..9c33c26d --- /dev/null +++ b/docs/api-endpoints/webdav/lock-element.md @@ -0,0 +1,4 @@ +# 🚧 LOCK` / -` Lock Element + +!> **Currently not implemented.** +This feature is reserved for the version [0.2.0](https://github.com/ember-nexus/api/milestone/1). diff --git a/docs/api-endpoints/webdav/mkcol-folder.md b/docs/api-endpoints/webdav/mkcol-folder.md new file mode 100644 index 00000000..e8a808b4 --- /dev/null +++ b/docs/api-endpoints/webdav/mkcol-folder.md @@ -0,0 +1,4 @@ +# 🚧 MKCOL` / -` Create Folder + +!> **Currently not implemented.** +This feature is reserved for the version [0.2.0](https://github.com/ember-nexus/api/milestone/1). diff --git a/docs/api-endpoints/webdav/move-element.md b/docs/api-endpoints/webdav/move-element.md new file mode 100644 index 00000000..49035e03 --- /dev/null +++ b/docs/api-endpoints/webdav/move-element.md @@ -0,0 +1,4 @@ +# 🚧 MOVE` / -` Move Element + +!> **Currently not implemented.** +This feature is reserved for the version [0.2.0](https://github.com/ember-nexus/api/milestone/1). diff --git a/docs/api-endpoints/webdav/propfind-element.md b/docs/api-endpoints/webdav/propfind-element.md new file mode 100644 index 00000000..2697e5c7 --- /dev/null +++ b/docs/api-endpoints/webdav/propfind-element.md @@ -0,0 +1,4 @@ +# 🚧 PROPFIND` / -` Find Element Property + +!> **Currently not implemented.** +This feature is reserved for the version [0.2.0](https://github.com/ember-nexus/api/milestone/1). diff --git a/docs/api-endpoints/webdav/proppatch-element.md b/docs/api-endpoints/webdav/proppatch-element.md new file mode 100644 index 00000000..75829f87 --- /dev/null +++ b/docs/api-endpoints/webdav/proppatch-element.md @@ -0,0 +1,4 @@ +# 🚧 PROPPATCH` / -` Change Element Property + +!> **Currently not implemented.** +This feature is reserved for the version [0.2.0](https://github.com/ember-nexus/api/milestone/1). diff --git a/docs/api-endpoints/webdav/unlock-element.md b/docs/api-endpoints/webdav/unlock-element.md new file mode 100644 index 00000000..e2f771e0 --- /dev/null +++ b/docs/api-endpoints/webdav/unlock-element.md @@ -0,0 +1,4 @@ +# 🚧 UNLOCK` / -` Unlock Element + +!> **Currently not implemented.** +This feature is reserved for the version [0.2.0](https://github.com/ember-nexus/api/milestone/1). diff --git a/docs/assets/custom.css b/docs/assets/custom.css index bbdb2102..2a729239 100755 --- a/docs/assets/custom.css +++ b/docs/assets/custom.css @@ -34,6 +34,8 @@ h5 code, h6 code { color: inherit !important; background: transparent !important; + margin: 0 !important; + padding: 0 !important; } td code { @@ -76,3 +78,46 @@ table { background-color: #f8f8f8; margin-bottom: 1rem; } + +.method-get, +.method-post, +.method-put, +.method-patch, +.method-delete, +.method-copy, +.method-lock, +.method-unlock, +.method-mkcol, +.method-move, +.method-propfind, +.method-proppatch { + font-weight: 700 !important; + font-family: 'Fira Code', 'Roboto Mono', Monaco, courier, monospace !important; +} + +.method-get { + color: #0074E4 !important; +} + +.method-post { + color: #4CAF50 !important; +} + +.method-put, +.method-patch { + color: #FFA726 !important; +} + +.method-delete { + color: #ff073a !important; +} + +.method-copy, +.method-lock, +.method-unlock, +.method-mkcol, +.method-move, +.method-propfind, +.method-proppatch { + color: #7E57C2 !important; +} diff --git a/docs/example/default-parameters.yaml b/docs/example/default-parameters.yaml index e6da8ccf..53cb5fc0 100644 --- a/docs/example/default-parameters.yaml +++ b/docs/example/default-parameters.yaml @@ -13,10 +13,6 @@ parameters: # performance problems may arise. max: 100 - # Null can be replaced by links to redirect to external problem documentation sites. - problemInstanceLinks: - 404-not-found: null - # Handles the /register endpoint. register: diff --git a/lib/EmberNexusBundle/src/Service/EmberNexusConfiguration.php b/lib/EmberNexusBundle/src/Service/EmberNexusConfiguration.php index 8023d7f0..f503c8fc 100644 --- a/lib/EmberNexusBundle/src/Service/EmberNexusConfiguration.php +++ b/lib/EmberNexusBundle/src/Service/EmberNexusConfiguration.php @@ -302,7 +302,7 @@ public function setTokenDefaultLifetimeInSeconds(int $tokenDefaultLifetimeInSeco return $this; } - public function getTokenMaxLifetimeInSeconds(): bool|int + public function getTokenMaxLifetimeInSeconds(): false|int { return $this->tokenMaxLifetimeInSeconds; } diff --git a/src/Controller/User/DeleteTokenController.php b/src/Controller/User/DeleteTokenController.php index a35da0e5..c65f05b4 100644 --- a/src/Controller/User/DeleteTokenController.php +++ b/src/Controller/User/DeleteTokenController.php @@ -8,6 +8,7 @@ use App\Security\AuthProvider; use App\Service\ElementManager; use LogicException; +use Predis\Client; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -17,6 +18,7 @@ class DeleteTokenController extends AbstractController public function __construct( private ElementManager $elementManager, private AuthProvider $authProvider, + private Client $redisClient, private Client401UnauthorizedExceptionFactory $client401UnauthorizedExceptionFactory, private Client404NotFoundExceptionFactory $client404NotFoundExceptionFactory ) { @@ -39,19 +41,27 @@ public function deleteToken(): Response throw $this->client401UnauthorizedExceptionFactory->createFromTemplate(); } + $hashedToken = $this->authProvider->getHashedToken(); + if (null === $hashedToken) { + throw $this->client401UnauthorizedExceptionFactory->createFromTemplate(); + } + $tokenUuid = $this->authProvider->getTokenUuid(); if (null === $tokenUuid) { throw new LogicException('Token must be provided.'); } - $element = $this->elementManager->getElement($tokenUuid); - if (null === $element) { + $tokenElement = $this->elementManager->getElement($tokenUuid); + if (null === $tokenElement) { throw $this->client404NotFoundExceptionFactory->createFromTemplate(); } - $this->elementManager->delete($element); + $this->elementManager->delete($tokenElement); $this->elementManager->flush(); - // todo: remove cached token from redis + $this->redisClient->expire( + $this->authProvider->getRedisTokenKeyFromHashedToken( + $hashedToken + ), 0); return new NoContentResponse(); } diff --git a/src/Controller/User/GetTokenController.php b/src/Controller/User/GetTokenController.php deleted file mode 100644 index 1a2b331c..00000000 --- a/src/Controller/User/GetTokenController.php +++ /dev/null @@ -1,62 +0,0 @@ -authProvider->getUserUuid(); - - if (!$userUuid) { - throw $this->client401UnauthorizedExceptionFactory->createFromTemplate(); - } - - if ($this->authProvider->isAnonymous()) { - throw $this->client401UnauthorizedExceptionFactory->createFromTemplate(); - } - - $cypherClient = $this->cypherEntityManager->getClient(); - $res = $cypherClient->runStatement(Statement::create( - "MATCH (user:User {id: \$userId})\n". - "MATCH (user)-[:OWNS]->(token:Token)\n". - "RETURN token.id\n". - "SKIP \$skip\n". - 'LIMIT $limit', - [ - 'userId' => $userUuid->toString(), - 'skip' => ($this->collectionService->getCurrentPage() - 1) * $this->collectionService->getPageSize(), - 'limit' => $this->collectionService->getPageSize(), - ] - )); - $tokenUuids = []; - foreach ($res as $resultSet) { - $tokenUuids[] = UuidV4::fromString($resultSet->get('token.id')); - } - - return $this->collectionService->buildCollectionFromUuids($tokenUuids, [], count($tokenUuids)); - } -} diff --git a/src/Controller/User/PostRegisterController.php b/src/Controller/User/PostRegisterController.php index a4d138fc..6c955216 100644 --- a/src/Controller/User/PostRegisterController.php +++ b/src/Controller/User/PostRegisterController.php @@ -2,6 +2,7 @@ namespace App\Controller\User; +use App\Factory\Exception\Client400BadContentExceptionFactory; use App\Factory\Exception\Client400MissingPropertyExceptionFactory; use App\Factory\Exception\Client400ReservedIdentifierExceptionFactory; use App\Factory\Exception\Client403ForbiddenExceptionFactory; @@ -27,6 +28,7 @@ public function __construct( private UrlGeneratorInterface $router, private UserPasswordHasher $userPasswordHasher, private EmberNexusConfiguration $emberNexusConfiguration, + private Client400BadContentExceptionFactory $client400BadContentExceptionFactory, private Client400MissingPropertyExceptionFactory $client400MissingPropertyExceptionFactory, private Client400ReservedIdentifierExceptionFactory $client400ReservedIdentifierExceptionFactory, private Client403ForbiddenExceptionFactory $client403ForbiddenExceptionFactory, @@ -60,6 +62,13 @@ public function postRegister(Request $request): Response } $password = $body['password']; + if (!array_key_exists('type', $body)) { + throw $this->client400MissingPropertyExceptionFactory->createFromTemplate('type', 'string'); + } + if ('User' !== $body['type']) { + throw $this->client400BadContentExceptionFactory->createFromTemplate('type', 'User', $body['type']); + } + $uniqueIdentifier = $this->emberNexusConfiguration->getRegisterUniqueIdentifier(); if (!array_key_exists($uniqueIdentifier, $data)) { throw $this->client400MissingPropertyExceptionFactory->createFromTemplate($uniqueIdentifier, 'string'); diff --git a/src/Controller/User/PostTokenController.php b/src/Controller/User/PostTokenController.php index 0ca9ba8e..ae65f593 100644 --- a/src/Controller/User/PostTokenController.php +++ b/src/Controller/User/PostTokenController.php @@ -2,21 +2,35 @@ namespace App\Controller\User; +use App\Factory\Exception\Client400BadContentExceptionFactory; +use App\Factory\Exception\Client400MissingPropertyExceptionFactory; use App\Factory\Exception\Client401UnauthorizedExceptionFactory; +use App\Factory\Exception\Server500LogicExceptionFactory; use App\Response\JsonResponse; -use App\Security\AuthProvider; use App\Security\TokenGenerator; +use App\Security\UserPasswordHasher; +use App\Service\ElementManager; +use EmberNexusBundle\Service\EmberNexusConfiguration; +use Laudis\Neo4j\Databags\Statement; +use Ramsey\Uuid\Uuid; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Syndesi\CypherEntityManager\Type\EntityManager as CypherEntityManager; class PostTokenController extends AbstractController { public function __construct( - private AuthProvider $authProvider, private TokenGenerator $tokenGenerator, - private Client401UnauthorizedExceptionFactory $client401UnauthorizedExceptionFactory + private Client400BadContentExceptionFactory $client400BadContentExceptionFactory, + private Client400MissingPropertyExceptionFactory $client400MissingPropertyExceptionFactory, + private Client401UnauthorizedExceptionFactory $client401UnauthorizedExceptionFactory, + private Server500LogicExceptionFactory $server500LogicExceptionFactory, + private EmberNexusConfiguration $emberNexusConfiguration, + private CypherEntityManager $cypherEntityManager, + private ElementManager $elementManager, + private UserPasswordHasher $userPasswordHasher ) { } @@ -27,30 +41,66 @@ public function __construct( )] public function postToken(Request $request): Response { - $userUuid = $this->authProvider->getUserUuid(); + $body = \Safe\json_decode($request->getContent(), true); - if (!$userUuid) { - throw $this->client401UnauthorizedExceptionFactory->createFromTemplate(); + if (!array_key_exists('type', $body)) { + throw $this->client400MissingPropertyExceptionFactory->createFromTemplate('type', 'string'); + } + if ('Token' !== $body['type']) { + throw $this->client400BadContentExceptionFactory->createFromTemplate('type', 'Token', $body['type']); } - $body = \Safe\json_decode($request->getContent(), true); /** * @var array $body - * #todo exception if JSON is not a document? */ + if (!array_key_exists('user', $body)) { + throw $this->client400MissingPropertyExceptionFactory->createFromTemplate('user', 'string'); + } + $uniqueIdentifierValue = $body['user']; + + $uniqueIdentifier = $this->emberNexusConfiguration->getRegisterUniqueIdentifier(); + $res = $this->cypherEntityManager->getClient()->runStatement(Statement::create( + sprintf( + 'MATCH (u:User {%s: $identifier}) RETURN u.id AS id', + $uniqueIdentifier, + ), + [ + 'identifier' => $uniqueIdentifierValue, + ] + )); + if (0 === count($res)) { + throw $this->client401UnauthorizedExceptionFactory->createFromTemplate(); + } + $userUuid = Uuid::fromString($res->first()->get('id')); + + $userElement = $this->elementManager->getElement($userUuid); + if (null === $userElement) { + throw $this->server500LogicExceptionFactory->createFromTemplate('Unable to load user element from id which was just returned.'); + } + + if (!array_key_exists('password', $body)) { + throw $this->client400MissingPropertyExceptionFactory->createFromTemplate('password', 'string'); + } + $password = $body['password']; + + if (true !== $this->userPasswordHasher->verifyPassword($password, $userElement->getProperty('_passwordHash'))) { + throw $this->client401UnauthorizedExceptionFactory->createFromTemplate(); + } + $lifetimeInSeconds = null; if (array_key_exists('lifetimeInSeconds', $body)) { $lifetimeInSeconds = (int) $body['lifetimeInSeconds']; } - $tokenName = null; - if (array_key_exists('name', $body)) { - $tokenName = (string) $body['name']; + $data = []; + if (array_key_exists('data', $body)) { + $data = $body['data']; } - $token = $this->tokenGenerator->createNewToken($userUuid, $tokenName, $lifetimeInSeconds); + $token = $this->tokenGenerator->createNewToken($userUuid, $data, $lifetimeInSeconds); return new JsonResponse([ + 'type' => '_TokenResponse', 'token' => $token, ]); } diff --git a/src/EventSystem/Exception/EventListener/ExceptionEventListener.php b/src/EventSystem/Exception/EventListener/ExceptionEventListener.php index b4778dec..e3cd9aec 100644 --- a/src/EventSystem/Exception/EventListener/ExceptionEventListener.php +++ b/src/EventSystem/Exception/EventListener/ExceptionEventListener.php @@ -7,7 +7,6 @@ use App\Response\ProblemJsonResponse; use Exception; use Psr\Log\LoggerInterface; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -17,7 +16,6 @@ class ExceptionEventListener public function __construct( private UrlGeneratorInterface $urlGenerator, private KernelInterface $kernel, - private ParameterBagInterface $bag, private LoggerInterface $logger, private Server500InternalServerErrorExceptionFactory $server500InternalServerErrorExceptionFactory ) { @@ -48,16 +46,6 @@ public function onKernelException(ExceptionEvent $event): void } catch (Exception $e) { } - // check if there are configured alternatives for the instance links - if ($this->bag->has('problemInstanceLinks') && $instance) { - $problemInstanceLinksConfig = $this->bag->get('problemInstanceLinks'); - if (is_array($problemInstanceLinksConfig)) { - if (array_key_exists($instance, $problemInstanceLinksConfig)) { - $instanceLink = $problemInstanceLinksConfig[$instance]; - } - } - } - $data = [ 'type' => $extendedException->getType(), 'title' => $extendedException->getTitle(), diff --git a/src/EventSystem/Request/EventListener/ApiKeyCheckOnKernelRequestEventListener.php b/src/EventSystem/Request/EventListener/ApiKeyCheckOnKernelRequestEventListener.php index 701c9b6e..43226ce7 100644 --- a/src/EventSystem/Request/EventListener/ApiKeyCheckOnKernelRequestEventListener.php +++ b/src/EventSystem/Request/EventListener/ApiKeyCheckOnKernelRequestEventListener.php @@ -47,7 +47,8 @@ public function onKernelRequest(RequestEvent $event): void $this->authProvider->setUserAndToken( $userUuidAndTokenUuidObject->getUserUuid(), - $userUuidAndTokenUuidObject->getTokenUuid() + $userUuidAndTokenUuidObject->getTokenUuid(), + $this->tokenGenerator->hashToken($token) ); } @@ -70,7 +71,7 @@ private function getUserUuidAndTokenUuidObjectFromTokenFromCypher(string $token) $userUuid = Uuid::fromString($res->first()->get('user.id')); $tokenUuid = Uuid::fromString($res->first()->get('token.id')); - $redisKey = sprintf('token:%s', $hashedToken); + $redisKey = $this->authProvider->getRedisTokenKeyFromHashedToken($hashedToken); $this->redisClient->hset($redisKey, 'token', $tokenUuid->toString()); $this->redisClient->hset($redisKey, 'user', $userUuid->toString()); $this->redisClient->expire($redisKey, 60 * 30); // 30 minutes @@ -81,12 +82,9 @@ private function getUserUuidAndTokenUuidObjectFromTokenFromCypher(string $token) ); } - private function getUserUuidAndTokenUuidFromTokenObjectFromRedis(string $token): ?UserUuidAndTokenUuidObject + private function getUserUuidAndTokenUuidFromTokenObjectFromRedis(string $rawToken): ?UserUuidAndTokenUuidObject { - $data = $this->redisClient->hgetall(sprintf( - 'token:%s', - $this->tokenGenerator->hashToken($token) - )); + $data = $this->redisClient->hgetall($this->authProvider->getRedisTokenKeyFromRawToken($rawToken)); if (0 === count($data)) { return null; diff --git a/src/Security/AuthProvider.php b/src/Security/AuthProvider.php index 4b5a4e82..3078bded 100755 --- a/src/Security/AuthProvider.php +++ b/src/Security/AuthProvider.php @@ -12,9 +12,11 @@ class AuthProvider private bool $isAnonymous; private ?UuidInterface $userUuid; private ?UuidInterface $tokenUuid = null; + private ?string $hashedToken = null; public function __construct( private ParameterBagInterface $bag, + private TokenGenerator $tokenGenerator, private Server500LogicExceptionFactory $server500LogicExceptionFactory ) { $anonymousUserUuid = $this->bag->get('anonymousUserUUID'); @@ -25,13 +27,28 @@ public function __construct( $this->isAnonymous = true; } + public function getRedisTokenKeyFromHashedToken(string $hashedToken): string + { + return sprintf( + 'token:%s', + $hashedToken + ); + } + + public function getRedisTokenKeyFromRawToken(string $rawToken): string + { + return $this->getRedisTokenKeyFromHashedToken($this->tokenGenerator->hashToken($rawToken)); + } + public function setUserAndToken( UuidInterface $userUuid = null, UuidInterface $tokenUuid = null, + string $hashedToken = null, bool $isAnonymous = false ): self { $this->userUuid = $userUuid; $this->tokenUuid = $tokenUuid; + $this->hashedToken = $hashedToken; $this->isAnonymous = $isAnonymous; return $this; @@ -47,6 +64,11 @@ public function getUserUuid(): ?UuidInterface return $this->userUuid; } + public function getHashedToken(): ?string + { + return $this->hashedToken; + } + public function getTokenUuid(): ?UuidInterface { return $this->tokenUuid; diff --git a/src/Security/TokenGenerator.php b/src/Security/TokenGenerator.php index 7de4f4cf..b04d8516 100755 --- a/src/Security/TokenGenerator.php +++ b/src/Security/TokenGenerator.php @@ -26,7 +26,10 @@ public function __construct( $this->encoder = new Base58(); } - public function createNewToken(UuidInterface $userUuid, string $name = null, int $lifetimeInSeconds = null): string + /** + * @param array $data + */ + public function createNewToken(UuidInterface $userUuid, array $data = [], int $lifetimeInSeconds = null): string { for ($i = 0; $i < 3; ++$i) { $token = $this->createToken(); @@ -48,28 +51,26 @@ public function createNewToken(UuidInterface $userUuid, string $name = null, int if (null === $lifetimeInSeconds) { $lifetimeInSeconds = $this->emberNexusConfiguration->getTokenDefaultLifetimeInSeconds(); } else { - if (false !== $this->emberNexusConfiguration->getTokenMaxLifetimeInSeconds()) { - if ($lifetimeInSeconds > $this->emberNexusConfiguration->getTokenMaxLifetimeInSeconds()) { - $lifetimeInSeconds = $this->emberNexusConfiguration->getTokenMaxLifetimeInSeconds(); + $tokenMaxLifetimeInSeconds = $this->emberNexusConfiguration->getTokenMaxLifetimeInSeconds(); + if (false !== $tokenMaxLifetimeInSeconds) { + if ($lifetimeInSeconds > $tokenMaxLifetimeInSeconds) { + $lifetimeInSeconds = $tokenMaxLifetimeInSeconds; } } if ($lifetimeInSeconds < $this->emberNexusConfiguration->getTokenMinLifetimeInSeconds()) { $lifetimeInSeconds = $this->emberNexusConfiguration->getTokenMinLifetimeInSeconds(); } } - /** - * @var int $lifetimeInSeconds - */ - $name ??= (new DateTime())->format('Y-m-d H:i:s'); $tokenUuid = Uuid::uuid4(); $tokenNode = (new NodeElement()) ->setLabel('Token') ->setIdentifier($tokenUuid) + ->addProperty('name', (new DateTime())->format('Y-m-d H:i:s')) + ->addProperties($data) ->addProperties([ 'hash' => $hash, 'expirationDate' => (new DateTime())->add(new DateInterval(sprintf('PT%sS', $lifetimeInSeconds))), - 'name' => $name, ]); $this->elementManager->create($tokenNode); $this->elementManager->flush(); diff --git a/src/Security/UserPasswordHasher.php b/src/Security/UserPasswordHasher.php index db7068aa..93780b86 100644 --- a/src/Security/UserPasswordHasher.php +++ b/src/Security/UserPasswordHasher.php @@ -8,4 +8,9 @@ public function hashPassword(string $password): string { return password_hash($password, PASSWORD_ARGON2I); } + + public function verifyPassword(string $password, string $passwordHash): bool + { + return password_verify($password, $passwordHash); + } } diff --git a/test-feature-prepare b/test-feature-prepare old mode 100644 new mode 100755 diff --git a/tests/FeatureTests/BaseRequestTestCase.php b/tests/FeatureTests/BaseRequestTestCase.php index b9f1ecd3..065b1eb3 100644 --- a/tests/FeatureTests/BaseRequestTestCase.php +++ b/tests/FeatureTests/BaseRequestTestCase.php @@ -28,72 +28,72 @@ public function runGetRequest(string $uri, string $token): ResponseInterface return $getRequest; } - public function runPostRequest(string $uri, string $token, array $data): ResponseInterface + public function runPostRequest(string $uri, ?string $token, array $data): ResponseInterface { return $this->runRequest('POST', $uri, $token, $data); } - public function runPutRequest(string $uri, string $token, array $data): ResponseInterface + public function runPutRequest(string $uri, ?string $token, array $data): ResponseInterface { return $this->runRequest('PUT', $uri, $token, $data); } - public function runPatchRequest(string $uri, string $token, array $data): ResponseInterface + public function runPatchRequest(string $uri, ?string $token, array $data): ResponseInterface { return $this->runRequest('PATCH', $uri, $token, $data); } - public function runDeleteRequest(string $uri, string $token): ResponseInterface + public function runDeleteRequest(string $uri, ?string $token): ResponseInterface { return $this->runRequest('DELETE', $uri, $token); } - public function runOptionsRequest(string $uri, string $token): ResponseInterface + public function runOptionsRequest(string $uri, ?string $token): ResponseInterface { return $this->runRequest('OPTIONS', $uri, $token); } - public function runHeadRequest(string $uri, string $token): ResponseInterface + public function runHeadRequest(string $uri, ?string $token): ResponseInterface { return $this->runRequest('HEAD', $uri, $token); } - public function runCopyRequest(string $uri, string $token): ResponseInterface + public function runCopyRequest(string $uri, ?string $token): ResponseInterface { return $this->runRequest('COPY', $uri, $token); } - public function runLockRequest(string $uri, string $token): ResponseInterface + public function runLockRequest(string $uri, ?string $token): ResponseInterface { return $this->runRequest('LOCK', $uri, $token); } - public function runMkcolRequest(string $uri, string $token): ResponseInterface + public function runMkcolRequest(string $uri, ?string $token): ResponseInterface { return $this->runRequest('MKCOL', $uri, $token); } - public function runMoveRequest(string $uri, string $token): ResponseInterface + public function runMoveRequest(string $uri, ?string $token): ResponseInterface { return $this->runRequest('MOVE', $uri, $token); } - public function runPropfindRequest(string $uri, string $token): ResponseInterface + public function runPropfindRequest(string $uri, ?string $token): ResponseInterface { return $this->runRequest('PROPFIND', $uri, $token); } - public function runProppatchRequest(string $uri, string $token): ResponseInterface + public function runProppatchRequest(string $uri, ?string $token): ResponseInterface { return $this->runRequest('PROPPATCH', $uri, $token); } - public function runUnlockRequest(string $uri, string $token): ResponseInterface + public function runUnlockRequest(string $uri, ?string $token): ResponseInterface { return $this->runRequest('UNLOCK', $uri, $token); } - public function runRequest(string $method, string $uri, string $token, array $data = null): ResponseInterface + public function runRequest(string $method, string $uri, string $token = null, array $data = null): ResponseInterface { $client = new Client([ 'base_uri' => $_ENV['API_DOMAIN'], @@ -101,15 +101,16 @@ public function runRequest(string $method, string $uri, string $token, array $da ]); $options = [ - 'headers' => [ - 'Authorization' => sprintf( - 'Bearer %s', - $token - ), - ], + 'headers' => [], ]; + if (null !== $token) { + $options['headers']['Authorization'] = sprintf( + 'Bearer %s', + $token + ); + } - if ($data) { + if (null !== $data) { $options['headers']['Content-Type'] = 'application/json; charset=utf-8'; $options['body'] = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } diff --git a/tests/FeatureTests/Endpoint/User/DeleteTokenTest.php b/tests/FeatureTests/Endpoint/User/DeleteTokenTest.php new file mode 100644 index 00000000..d6a58829 --- /dev/null +++ b/tests/FeatureTests/Endpoint/User/DeleteTokenTest.php @@ -0,0 +1,31 @@ +runGetRequest('/', self::TOKEN_1); + $this->assertIsCollectionResponse($indexResponse); + + $response = $this->runDeleteRequest('/token', self::TOKEN_1); + $this->assertNoContentResponse($response); + + $failingIndexResponse = $this->runGetRequest('/', self::TOKEN_1); + $this->assertIsProblemResponse($failingIndexResponse, 401); + + $newIndexResponse = $this->runGetRequest('/', self::TOKEN_2); + $this->assertIsCollectionResponse($newIndexResponse); + + $countBeforeDeletion = json_decode((string) $indexResponse->getBody(), true)['totalNodes']; + $countAfterDeletion = json_decode((string) $newIndexResponse->getBody(), true)['totalNodes']; + + $this->assertSame($countBeforeDeletion, $countAfterDeletion + 1); + } +} diff --git a/tests/FeatureTests/Endpoint/User/PostRegisterTest.php b/tests/FeatureTests/Endpoint/User/PostRegisterTest.php new file mode 100644 index 00000000..9f1724b6 --- /dev/null +++ b/tests/FeatureTests/Endpoint/User/PostRegisterTest.php @@ -0,0 +1,121 @@ +runPostRequest( + '/register', + null, + [ + 'type' => 'User', + 'password' => '1234', + 'data' => [ + 'email' => 'user1@register.user.endpoint.localhost.de', + ], + ] + ); + + $this->assertIsCreatedResponse($response); + } + + public function testPostRegisterWithoutType(): void + { + $response = $this->runPostRequest( + '/register', + null, + [ + 'password' => '1234', + 'data' => [ + 'email' => 'user2@register.user.endpoint.localhost.de', + ], + ] + ); + + $this->assertIsProblemResponse($response, 400); + } + + public function testPostRegisterWithWrongType(): void + { + $response = $this->runPostRequest( + '/register', + null, + [ + 'type' => 'NotAUser', + 'password' => '1234', + 'data' => [ + 'email' => 'user3@register.user.endpoint.localhost.de', + ], + ] + ); + + $this->assertIsProblemResponse($response, 400); + } + + public function testPostRegisterWithNoPassword(): void + { + $response = $this->runPostRequest( + '/register', + null, + [ + 'type' => 'User', + 'data' => [ + 'email' => 'user4@register.user.endpoint.localhost.de', + ], + ] + ); + + $this->assertIsProblemResponse($response, 400); + } + + public function testPostRegisterWithNoEmail(): void + { + $response = $this->runPostRequest( + '/register', + null, + [ + 'type' => 'User', + 'password' => '1234', + 'data' => [ + ], + ] + ); + + $this->assertIsProblemResponse($response, 400); + } + + public function testPostRegisterFailsForDuplicateEmail(): void + { + $response1 = $this->runPostRequest( + '/register', + null, + [ + 'type' => 'User', + 'password' => '1234', + 'data' => [ + 'email' => 'user5@register.user.endpoint.localhost.de', + ], + ] + ); + + $this->assertIsCreatedResponse($response1); + + $response2 = $this->runPostRequest( + '/register', + null, + [ + 'type' => 'User', + 'password' => '1234', + 'data' => [ + 'email' => 'user5@register.user.endpoint.localhost.de', + ], + ] + ); + + $this->assertIsProblemResponse($response2, 400); + } +} diff --git a/tests/FeatureTests/Endpoint/User/PostTokenTest.php b/tests/FeatureTests/Endpoint/User/PostTokenTest.php new file mode 100644 index 00000000..f66013be --- /dev/null +++ b/tests/FeatureTests/Endpoint/User/PostTokenTest.php @@ -0,0 +1,136 @@ +runGetRequest('/', self::TOKEN); + $getIndexResponseCount = json_decode((string) $getIndexResponse->getBody(), true)['totalNodes']; + + $response = $this->runPostRequest( + '/token', + null, + [ + 'type' => 'Token', + 'user' => self::EMAIL, + 'password' => self::PASSWORD, + ] + ); + + echo "\n\n".((string) $response->getBody())."\n\n"; + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('application/json; charset=utf-8', $response->getHeader('content-type')[0]); + $body = \Safe\json_decode((string) $response->getBody(), true); + $this->assertSame('_TokenResponse', $body['type']); + $this->assertArrayHasKey('token', $body); + + // test that new token works + $getIndexResponseFromNewToken = $this->runGetRequest('/', $body['token']); + $getIndexResponseFromNewTokenCount = json_decode((string) $getIndexResponseFromNewToken->getBody(), true)['totalNodes']; + + // test that new token is counted as new element + $this->assertSame($getIndexResponseFromNewTokenCount, $getIndexResponseCount + 1); + } + + public function testPostTokenWithLifetime(): void + { + $getIndexResponse = $this->runGetRequest('/', self::TOKEN); + $getIndexResponseCount = json_decode((string) $getIndexResponse->getBody(), true)['totalNodes']; + + $response = $this->runPostRequest( + '/token', + null, + [ + 'type' => 'Token', + 'user' => self::EMAIL, + 'password' => self::PASSWORD, + 'lifetime' => 3600, + ] + ); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('application/json; charset=utf-8', $response->getHeader('content-type')[0]); + $body = \Safe\json_decode((string) $response->getBody(), true); + $this->assertSame('_TokenResponse', $body['type']); + $this->assertArrayHasKey('token', $body); + + // test that new token works + $getIndexResponseFromNewToken = $this->runGetRequest('/', $body['token']); + $getIndexResponseFromNewTokenCount = json_decode((string) $getIndexResponseFromNewToken->getBody(), true)['totalNodes']; + + // test that new token is counted as new element + $this->assertSame($getIndexResponseFromNewTokenCount, $getIndexResponseCount + 1); + } + + public function testPostTokenWithLifetimeUnderMinimumGetsSetToMinimum(): void + { + $getIndexResponse = $this->runGetRequest('/', self::TOKEN); + $getIndexResponseCount = json_decode((string) $getIndexResponse->getBody(), true)['totalNodes']; + + $response = $this->runPostRequest( + '/token', + null, + [ + 'type' => 'Token', + 'user' => self::EMAIL, + 'password' => self::PASSWORD, + 'lifetime' => 0, + ] + ); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('application/json; charset=utf-8', $response->getHeader('content-type')[0]); + $body = \Safe\json_decode((string) $response->getBody(), true); + $this->assertSame('_TokenResponse', $body['type']); + $this->assertArrayHasKey('token', $body); + + // test that new token works + $getIndexResponseFromNewToken = $this->runGetRequest('/', $body['token']); + $getIndexResponseFromNewTokenCount = json_decode((string) $getIndexResponseFromNewToken->getBody(), true)['totalNodes']; + + // test that new token is counted as new element + $this->assertSame($getIndexResponseFromNewTokenCount, $getIndexResponseCount + 1); + } + + public function testPostTokenWithLifetimeAboveMaximumGetsSetToMaximum(): void + { + $getIndexResponse = $this->runGetRequest('/', self::TOKEN); + $getIndexResponseCount = json_decode((string) $getIndexResponse->getBody(), true)['totalNodes']; + + $response = $this->runPostRequest( + '/token', + null, + [ + 'type' => 'Token', + 'user' => self::EMAIL, + 'password' => self::PASSWORD, + 'lifetime' => 100 * 365 * 24 * 3600, // 100 years + ] + ); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('application/json; charset=utf-8', $response->getHeader('content-type')[0]); + $body = \Safe\json_decode((string) $response->getBody(), true); + $this->assertSame('_TokenResponse', $body['type']); + $this->assertArrayHasKey('token', $body); + + // test that new token works + $getIndexResponseFromNewToken = $this->runGetRequest('/', $body['token']); + $getIndexResponseFromNewTokenCount = json_decode((string) $getIndexResponseFromNewToken->getBody(), true)['totalNodes']; + + // test that new token is counted as new element + $this->assertSame($getIndexResponseFromNewTokenCount, $getIndexResponseCount + 1); + } +}