From f9441e036b3820ee0aa417387620f6217f8cedc4 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Mon, 19 Feb 2024 16:04:13 +0100 Subject: [PATCH 01/21] Added an API endpoint to fetch data for the dashboard --- settings/routes.php | 2 + .../Api/DashboardController.php | 89 +++++++++++++++++++ .../Dashboard/Dto/DashboardRowList.php | 18 ++++ 3 files changed, 109 insertions(+) create mode 100644 src/HttpController/Api/DashboardController.php diff --git a/settings/routes.php b/settings/routes.php index 7c49d23d3..be5ebe848 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -204,6 +204,8 @@ function addApiRoutes(RouterService $routerService, FastRoute\RouteCollector $ro $routes->add('GET', '/openapi', [Api\OpenApiController::class, 'getSchema']); + $routes->add('GET', '/users/{username:[a-zA-Z0-9]+}/dashboard', [Api\DashboardController::class, 'getDashboardData']); + $routeUserHistory = '/users/{username:[a-zA-Z0-9]+}/history/movies'; $routes->add('GET', $routeUserHistory, [Api\HistoryController::class, 'getHistory'], [Api\Middleware\IsAuthorizedToReadUserData::class]); $routes->add('POST', $routeUserHistory, [Api\HistoryController::class, 'addToHistory'], [Api\Middleware\IsAuthorizedToWriteUserData::class]); diff --git a/src/HttpController/Api/DashboardController.php b/src/HttpController/Api/DashboardController.php new file mode 100644 index 000000000..85cedbec5 --- /dev/null +++ b/src/HttpController/Api/DashboardController.php @@ -0,0 +1,89 @@ +userPageAuthorizationChecker->findUserIdIfCurrentVisitorIsAllowedToSeeUser((string)$request->getRouteParameters()['username']); + if ($userId === null) { + return Response::createForbiddenRedirect($request->getPath()); + } + + $currentUserId = null; + if ($this->authenticationService->isUserAuthenticated() === true) { + $currentUserId = $this->authenticationService->getCurrentUserId(); + } + + $dashboardRows = $this->dashboardFactory->createDashboardRowsForUser($this->userApi->fetchUser($userId)); + + $response = [ + 'users' => $this->userPageAuthorizationChecker->fetchAllVisibleUsernamesForCurrentVisitor(), + 'totalPlayCount' => $this->movieApi->fetchTotalPlayCount($userId), + 'uniqueMoviesCount' => $this->movieApi->fetchTotalPlayCountUnique($userId), + 'totalHoursWatched' => $this->movieHistoryApi->fetchTotalHoursWatched($userId), + 'averagePersonalRating' => $this->movieHistoryApi->fetchAveragePersonalRating($userId), + 'averagePlaysPerDay' => $this->movieHistoryApi->fetchAveragePlaysPerDay($userId), + 'averageRuntime' => $this->movieHistoryApi->fetchAverageRuntime($userId), + 'dashboardRows' => $dashboardRows->asArray(), + 'lastPlays' => [], + 'mostWatchedActors' => [], + 'mostWatchedActresses' => [], + 'mostWatchedDirectors' => [], + 'mostWatchedLanguages' => [], + 'mostWatchedGenres' => [], + 'mostWatchedProductionCompanies' => [], + 'mostWatchedReleaseYears' => [], + 'watchlistItems' => [], + ]; + + foreach($dashboardRows as $row) { + if($row->isExtended()) { + if($row->isLastPlays()) { + $response['lastPlays'] = $this->movieHistoryApi->fetchLastPlays($userId); + } else if($row->isMostWatchedActors()) { + $response['mostWatchedActors'] = $this->movieHistoryApi->fetchActors($userId, 6, 1, gender: Gender::createMale(), personFilterUserId: $currentUserId); + } else if($row->isMostWatchedActresses()) { + $response['mostWatchedActresses'] = $this->movieHistoryApi->fetchActors($userId, 6, 1, gender: Gender::createFemale(), personFilterUserId: $currentUserId); + } else if($row->isMostWatchedDirectors()) { + $response['mostWatchedDirectors'] = $this->movieHistoryApi->fetchDirectors($userId, 6, 1, personFilterUserId: $currentUserId); + } else if($row->isMostWatchedLanguages()) { + $response['mostWatchedLanguages'] = $this->movieHistoryApi->fetchMostWatchedLanguages($userId); + } else if($row->isMostWatchedGenres()) { + $response['mostWatchedGenres'] = $this->movieHistoryApi->fetchMostWatchedGenres($userId); + } else if($row->isMostWatchedProductionCompanies()) { + $response['mostWatchedProductionCompanies'] = $this->movieHistoryApi->fetchMostWatchedProductionCompanies($userId, 12); + } else if($row->isMostWatchedReleaseYears()) { + $response['mostWatchedReleaseYears'] = $this->movieHistoryApi->fetchMostWatchedReleaseYears($userId); + } else if($row->isWatchlist()) { + $response['watchlistItems'] = $this->movieWatchlistApi->fetchWatchlistPaginated($userId, 6, 1); + } + } + } + return Response::createJson(Json::encode($response)); + } +} diff --git a/src/Service/Dashboard/Dto/DashboardRowList.php b/src/Service/Dashboard/Dto/DashboardRowList.php index c1aa9b2a4..6195b8319 100644 --- a/src/Service/Dashboard/Dto/DashboardRowList.php +++ b/src/Service/Dashboard/Dto/DashboardRowList.php @@ -34,4 +34,22 @@ public function addAtOffset(int $position, DashboardRow $dashboardRow) : void ksort($this->data); } + + public function asArray() : array + { + $serialized = []; + /** + * @var $row DashboardRow + * @var $this->data array + */ + foreach($this->data as $row) { + array_push($serialized, [ + 'row' => $row->getName(), + 'id' => $row->getId(), + 'isExtended' => $row->isExtended(), + 'isVisible' => $row->isVisible() + ]); + } + return $serialized; + } } From efb12be7438f9ef5a26811851be3bb5f911f917f Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Mon, 19 Feb 2024 16:43:39 +0100 Subject: [PATCH 02/21] Added OpenAPI spec for the dashboard API endpoint --- docs/openapi.json | 252 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) diff --git a/docs/openapi.json b/docs/openapi.json index 83a6e182e..3bde7dd25 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -6,6 +6,258 @@ }, "servers": [], "paths": { + "\/users\/{username}\/dashboard": { + "get": { + "tags": [ + "Dashboard" + ], + "summary": "Get all the data that's shown in the dashboard", + "description": "Get all the statistics and the order of the rows in the dashboard. When a row is collapsed / hidden by default, the data for the hidden row isn't sent.", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "User is allowed to see the statistics of the requested user.", + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "users": { + "type": "object", + "example": [ + { + "name": "user1" + }, + { + "name": "user2" + } + ] + }, + "totalPlayCount": { + "$ref": "#/components/schemas/plays" + }, + "uniqueMoviesCount" : { + "type": "integer", + "example": 1 + }, + "totalHoursWatched": { + "type": "number", + "example": 1 + }, + "averagePersonalRating": { + "type": "number", + "example": 1 + }, + "averagePlaysPerDay": { + "type": "number", + "example": 1 + }, + "averageRuntime": { + "type": "number", + "example": 1 + }, + "dashboardRows": { + "type": "array", + "description": "An array containing JSON objects of the dashboard rows. The order of the objects is the order of the rows.", + "example": [ + { + "row": "Last Plays", + "id": 0, + "isExtended": true, + "isVisible": true + }, + { + "row": "Latest in Watchlist", + "id": 9, + "isExtended": true, + "isVisible": true + }, + { + "row": "Most watched Actors", + "id": 1, + "isExtended": false, + "isVisible": true + }, + { + "row": "Most watched Actresses", + "id": 2, + "isExtended": false, + "isVisible": true + }, + { + "row": "Most watched Directors", + "id": 3, + "isExtended": false, + "isVisible": true + }, + { + "row": "Most watched Genres", + "id": 4, + "isExtended": false, + "isVisible": true + }, + { + "row": "Most watched Languages", + "id": 8, + "isExtended": false, + "isVisible": true + }, + { + "row": "Most watched Production Companies", + "id": 6, + "isExtended": false, + "isVisible": true + }, + { + "row": "Most watched Release Years", + "id": 7, + "isExtended": false, + "isVisible": true + } + ] + }, + "lastPlays": { + "type": "array", + "description": "An array containing JSON objects of the last played movies based on date.", + "example": [ + { + "$ref": "#/components/schemas/movie" + } + ] + }, + "mostWatchedActors": { + "type": "array", + "description": "An array containing JSON objects of the most watched male actors.", + "example": [ + { + "id": 1, + "name": "Actor name", + "uniqueCount": 4, + "totalCount": 4, + "gender": "m", + "tmdb_poster_path": "/path_to_poster_on_tmdb.jpg", + "poster_path": "/storage/images/person/path_to_poster_on_local_storage.jpg" + } + ] + }, + "mostWatchedActresses": { + "type": "array", + "description": "An array containing JSON objects of the most watched actresses.", + "example": [ + { + "id": 1, + "name": "Actress name", + "uniqueCount": 2, + "totalCount": 2, + "gender": "f", + "tmdb_poster_path": "/path_to_poster_on_tmdb.jpg", + "poster_path": "/storage/images/person/path_to_poster_on_local_storage.jpg" + } + ] + }, + "mostWatchedDirectors": { + "type": "array", + "description": "An array containing JSON objects of the most watched directors.", + "example": [ + { + "id": 1, + "name": "Director name", + "uniqueCount": 2, + "totalCount": 2, + "gender": "m", + "tmdb_poster_path": "/path_to_poster_on_tmdb.jpg", + "poster_path": "/storage/images/person/path_to_poster_on_local_storage.jpg" + } + ] + }, + "mostWatchedLanguages": { + "type": "array", + "description": "An array containing JSON objects of the most watched languages.", + "example": [ + { + "language": "en", + "count": 7, + "name": "English", + "code": "en" + } + ] + }, + "mostWatchedGenres": { + "type": "array", + "description": "An array containing JSON objects of the most watched genres.", + "example": [ + { + "name": "Adventure", + "count": 7 + }, + { + "name": "Action", + "count": 6 + }, + { + "name": "Science Fiction", + "count": 3 + }, + { + "name": "Fantasy", + "count": 1 + } + ] + }, + "mostWatchedProductionCompanies": { + "type": "array", + "description": "An array containing JSON objects of the most watched production companies and the watched movies they produced.", + "example": [ + { + "name": "Lucasfilm Ltd.", + "count": 4, + "origin_country": "US", + "movies": [ + "Indiana Jones and the Temple of Doom", + "Raiders of the Lost Ark", + "Indiana Jones and the Kingdom of the Crystal Skull", + "Indiana Jones and the Last Crusade" + ] + } + ] + }, + "mostWatchedReleaseYears": { + "type": "array", + "description": "An array containing JSON objects of the most watched release years.", + "example": [ + { + "name": 2023, + "count": 1 + } + ] + }, + "watchlistItems": { + "type": "array", + "description": "An array containing JSON objects of the items in the watchlist.", + "example": [ + { + "$ref": "#/components/schemas/movie" + } + ] + } + } + } + } + } + } + } + } + }, "\/users\/{username}\/history\/movies": { "get": { "tags": [ From 3b4d033b5e5165d291fa799b14e9e9e5a3a28186 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Mon, 19 Feb 2024 18:02:40 +0100 Subject: [PATCH 03/21] Added 403 and 404 to OpenAPI and added HTTP test --- docs/openapi.json | 6 ++++++ src/HttpController/Api/DashboardController.php | 14 +++++++++----- tests/rest/api/user-dashboard.http | 5 +++++ 3 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 tests/rest/api/user-dashboard.http diff --git a/docs/openapi.json b/docs/openapi.json index 3bde7dd25..595d8a487 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -254,6 +254,12 @@ } } } + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" } } } diff --git a/src/HttpController/Api/DashboardController.php b/src/HttpController/Api/DashboardController.php index 85cedbec5..f047c0612 100644 --- a/src/HttpController/Api/DashboardController.php +++ b/src/HttpController/Api/DashboardController.php @@ -29,15 +29,19 @@ public function __construct( public function getDashboardData(Request $request) : Response { - $userId = $this->userPageAuthorizationChecker->findUserIdIfCurrentVisitorIsAllowedToSeeUser((string)$request->getRouteParameters()['username']); - if ($userId === null) { - return Response::createForbiddenRedirect($request->getPath()); - } - $currentUserId = null; if ($this->authenticationService->isUserAuthenticated() === true) { $currentUserId = $this->authenticationService->getCurrentUserId(); } + $requestedUser = $this->userApi->findUserByName((string)$request->getRouteParameters()['username']); + if ($requestedUser === null) { + return Response::createNotFound(); + } + + if ($this->userPageAuthorizationChecker->findUserIfCurrentVisitorIsAllowedToSeeUser($requestedUser->getName()) === false) { + return Response::createForbidden(); + } + $userId = $requestedUser->getId(); $dashboardRows = $this->dashboardFactory->createDashboardRowsForUser($this->userApi->fetchUser($userId)); diff --git a/tests/rest/api/user-dashboard.http b/tests/rest/api/user-dashboard.http new file mode 100644 index 000000000..e15bd7011 --- /dev/null +++ b/tests/rest/api/user-dashboard.http @@ -0,0 +1,5 @@ +GET http://127.0.0.1/api/users/{{username}}/dashboard +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: REST Unit Test \ No newline at end of file From 4e7ec34187b3c11f090ab7a2845b2827bfc5c969 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Thu, 22 Feb 2024 22:31:14 +0100 Subject: [PATCH 04/21] Moved `MovieRatingController` to API routes and added OpenAPI specs and HTTP tests --- docs/openapi.json | 144 ++++++++++++++++-- public/js/app.js | 2 +- public/js/movie.js | 10 +- settings/routes.php | 3 + .../Movie => Api}/MovieRatingController.php | 10 +- tests/rest/api/movie-rating.http | 18 +++ 6 files changed, 162 insertions(+), 25 deletions(-) rename src/HttpController/{Web/Movie => Api}/MovieRatingController.php (82%) create mode 100644 tests/rest/api/movie-rating.http diff --git a/docs/openapi.json b/docs/openapi.json index 4e195b930..4c8b3f67e 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -876,6 +876,115 @@ ] } }, + "/users/{username}/movies/{id}/rating": { + "post": { + "tags": [ + "Rating" + ], + "summary": "Update the rating of an user for a movie", + "description": "Update the rating of an user for a movie", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "Movary ID of the movie", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "rating": { + "type": "integer", + "description": "Rating of the movie. It has to be an integer in the range of 0 - 10. If the rating is 0, then the rating will be deleted.", + "example": 10, + "minimum": 0, + "maximum": 10 + } + } + } + } + } + } + }, + "responses": { + "204": { + "$ref": "#/components/responses/204" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + } + }, + "security": [ + { + "authToken": [], + "cookieauth": [] + } + ] + }, + }, + "\/fetchMovieRatingByTmdbdId": { + "get": { + "tags": [ + "Rating" + ], + "summary": "Get movie rating", + "description": "Get the movie rating from the current user. The movie is found by using the TMDB ID", + "parameters": [ + { + "name": "tmdbId", + "description": "The ID of the movie from TMDB", + "in": "query", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "The TMDB ID was valid and the ratings have been returned.", + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "personalRating": { + "type": "integer", + "description": "The rating of the movie by the user. It will be null if there is no rating.", + "minimum": 1, + "maximum": 10 + } + }, + } + } + } + }, + "403": { + "$ref": "#/components/responses/403" + } + } + } + }, "\/movies\/search": { "get": { "tags": [ @@ -1072,15 +1181,17 @@ "Authentication" ], "description": "Create an authentication token via email, password and optionally totp code. Add the token as X-Auth-Token header to further requests. Token lifetime 1d default, 30d with rememberMe.", - "parameters": [{ - "in": "header", - "name": "X-Movary-Client", - "schema": { - "type": "string" - }, - "required": true, - "example": "Client Name" - }], + "parameters": [ + { + "in": "header", + "name": "X-Movary-Client", + "schema": { + "type": "string" + }, + "required": true, + "example": "Client Name" + } + ], "requestBody": { "description": "The credentials and optionally a two-factor authentication code", "required": true, @@ -1088,7 +1199,10 @@ "application/json": { "schema": { "type": "object", - "required": ["email", "password"], + "required": [ + "email", + "password" + ], "properties": { "email": { "type": "string", @@ -1135,12 +1249,12 @@ } }, "headers": { - "Set-Cookie": { - "schema": { - "type": "string", - "example": "" - } + "Set-Cookie": { + "schema": { + "type": "string", + "example": "" } + } } }, "400": { diff --git a/public/js/app.js b/public/js/app.js index 91a1f5204..847b8309d 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -430,7 +430,7 @@ function getCurrentDate() { * Rating star logic starting here */ async function fetchRating(tmdbId) { - const response = await fetch('/fetchMovieRatingByTmdbdId?tmdbId=' + tmdbId) + const response = await fetch('/api/fetchMovieRatingByTmdbdId?tmdbId=' + tmdbId) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) diff --git a/public/js/movie.js b/public/js/movie.js index fa7323f00..6d65f85e1 100644 --- a/public/js/movie.js +++ b/public/js/movie.js @@ -58,14 +58,16 @@ function getRouteUsername() { } function saveRating() { - let newRating = getRatingFromStars('editRatingModal') + let newRating = getRatingFromStars('editRatingModal'); - fetch('/users/' + getRouteUsername() + '/movies/' + getMovieId() + '/rating', { + fetch('/api/users/' + getRouteUsername() + '/movies/' + getMovieId() + '/rating', { method: 'post', headers: { - 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8' + 'Content-type': 'application/json' }, - body: 'rating=' + newRating + body: JSON.stringify({ + 'rating': newRating + }) }).then(function (response) { if (response.ok === false) { addAlert('editRatingModalDiv', 'Could not update rating.', 'danger') diff --git a/settings/routes.php b/settings/routes.php index a39d98018..7625ab02e 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -223,6 +223,9 @@ function addApiRoutes(RouterService $routerService, FastRoute\RouteCollector $ro $routes->add('GET', '/movies/search', [Api\MovieSearchController::class, 'search'], [Api\Middleware\IsAuthenticated::class]); + $routes->add('POST', '/users/{username:[a-zA-Z0-9]+}/movies/{id:\d+}/rating', [Api\MovieRatingController::class, 'updateRating'], [Api\Middleware\IsAuthorizedToWriteUserData::class]); + $routes->add('GET', '/fetchMovieRatingByTmdbdId', [Api\MovieRatingController::class, 'fetchMovieRatingByTmdbdId'], [Api\Middleware\IsAuthenticated::class]); + $routes->add('POST', '/webhook/plex/{id:.+}', [Api\PlexController::class, 'handlePlexWebhook']); $routes->add('POST', '/webhook/jellyfin/{id:.+}', [Api\JellyfinController::class, 'handleJellyfinWebhook']); $routes->add('POST', '/webhook/emby/{id:.+}', [Api\EmbyController::class, 'handleEmbyWebhook']); diff --git a/src/HttpController/Web/Movie/MovieRatingController.php b/src/HttpController/Api/MovieRatingController.php similarity index 82% rename from src/HttpController/Web/Movie/MovieRatingController.php rename to src/HttpController/Api/MovieRatingController.php index 8bd1a1acc..e82b0ff0a 100644 --- a/src/HttpController/Web/Movie/MovieRatingController.php +++ b/src/HttpController/Api/MovieRatingController.php @@ -1,6 +1,6 @@ authenticationService->getCurrentUserId(); + $userId = $this->authenticationService->getUserIdByApiToken($request); $tmdbId = $request->getGetParameters()['tmdbId'] ?? null; $userRating = null; @@ -39,7 +39,7 @@ public function fetchMovieRatingByTmdbdId(Request $request) : Response public function updateRating(Request $request) : Response { - $userId = $this->authenticationService->getCurrentUserId(); + $userId = $this->authenticationService->getUserIdByApiToken($request); if ($this->userApi->fetchUser($userId)->getName() !== $request->getRouteParameters()['username']) { return Response::createForbidden(); @@ -47,14 +47,14 @@ public function updateRating(Request $request) : Response $movieId = (int)$request->getRouteParameters()['id']; - $postParameters = $request->getPostParameters(); + $postParameters = Json::decode($request->getBody()); $personalRating = null; if (empty($postParameters['rating']) === false && $postParameters['rating'] !== 0) { $personalRating = PersonalRating::create((int)$postParameters['rating']); } - $this->movieApi->updateUserRating($movieId, $this->authenticationService->getCurrentUserId(), $personalRating); + $this->movieApi->updateUserRating($movieId, $userId, $personalRating); return Response::create(StatusCode::createNoContent()); } diff --git a/tests/rest/api/movie-rating.http b/tests/rest/api/movie-rating.http new file mode 100644 index 000000000..605454357 --- /dev/null +++ b/tests/rest/api/movie-rating.http @@ -0,0 +1,18 @@ +@tmdbId = 329 +GET http://127.0.0.1/api/fetchMovieRatingByTmdbdId?tmdbId={{tmdbId}} +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Auth-Token: {{xAuthToken}} + +### + +POST http://127.0.0.1/api/users/{{username}}/movies/1/rating +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Auth-Token: {{xAuthToken}} + +{ + "rating": 10 +} \ No newline at end of file From 70afcdc00c78f0c66405d521b72f6d48b5625cc5 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Fri, 23 Feb 2024 12:11:08 +0100 Subject: [PATCH 05/21] Removed old endpoints --- settings/routes.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/settings/routes.php b/settings/routes.php index 7625ab02e..4bd272184 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -186,13 +186,8 @@ function addWebRoutes(RouterService $routerService, FastRoute\RouteCollector $ro Web\HistoryController::class, 'createHistoryEntry' ], [Web\Middleware\UserIsAuthenticated::class]); - $routes->add('POST', '/users/{username:[a-zA-Z0-9]+}/movies/{id:\d+}/rating', [ - Web\Movie\MovieRatingController::class, - 'updateRating' - ], [Web\Middleware\UserIsAuthenticated::class]); $routes->add('POST', '/log-movie', [Web\HistoryController::class, 'logMovie'], [Web\Middleware\UserIsAuthenticated::class]); $routes->add('POST', '/add-movie-to-watchlist', [Web\WatchlistController::class, 'addMovieToWatchlist'], [Web\Middleware\UserIsAuthenticated::class]); - $routes->add('GET', '/fetchMovieRatingByTmdbdId', [Web\Movie\MovieRatingController::class, 'fetchMovieRatingByTmdbdId'], [Web\Middleware\UserIsAuthenticated::class]); $routerService->addRoutesToRouteCollector($routeCollector, $routes); } From 29ab92cbf85f4592a6c06592a9329d824cd94479 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Fri, 23 Feb 2024 13:14:17 +0100 Subject: [PATCH 06/21] Fix tests --- settings/routes.php | 2 +- .../Api/DashboardController.php | 31 +++++++------------ .../Dashboard/Dto/DashboardRowList.php | 8 ++--- 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/settings/routes.php b/settings/routes.php index be5ebe848..e144d4b83 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -204,7 +204,7 @@ function addApiRoutes(RouterService $routerService, FastRoute\RouteCollector $ro $routes->add('GET', '/openapi', [Api\OpenApiController::class, 'getSchema']); - $routes->add('GET', '/users/{username:[a-zA-Z0-9]+}/dashboard', [Api\DashboardController::class, 'getDashboardData']); + $routes->add('GET', '/users/{username:[a-zA-Z0-9]+}/dashboard', [Api\DashboardController::class, 'getDashboardData'], [Api\Middleware\IsAuthorizedToReadUserData::class]); $routeUserHistory = '/users/{username:[a-zA-Z0-9]+}/history/movies'; $routes->add('GET', $routeUserHistory, [Api\HistoryController::class, 'getHistory'], [Api\Middleware\IsAuthorizedToReadUserData::class]); diff --git a/src/HttpController/Api/DashboardController.php b/src/HttpController/Api/DashboardController.php index f047c0612..997b2afe1 100644 --- a/src/HttpController/Api/DashboardController.php +++ b/src/HttpController/Api/DashboardController.php @@ -23,24 +23,15 @@ public function __construct( private readonly UserPageAuthorizationChecker $userPageAuthorizationChecker, private readonly DashboardFactory $dashboardFactory, private readonly UserApi $userApi, - private readonly Authentication $authenticationService, ) { } public function getDashboardData(Request $request) : Response { - $currentUserId = null; - if ($this->authenticationService->isUserAuthenticated() === true) { - $currentUserId = $this->authenticationService->getCurrentUserId(); - } $requestedUser = $this->userApi->findUserByName((string)$request->getRouteParameters()['username']); if ($requestedUser === null) { return Response::createNotFound(); } - - if ($this->userPageAuthorizationChecker->findUserIfCurrentVisitorIsAllowedToSeeUser($requestedUser->getName()) === false) { - return Response::createForbidden(); - } $userId = $requestedUser->getId(); $dashboardRows = $this->dashboardFactory->createDashboardRowsForUser($this->userApi->fetchUser($userId)); @@ -69,21 +60,21 @@ public function getDashboardData(Request $request) : Response if($row->isExtended()) { if($row->isLastPlays()) { $response['lastPlays'] = $this->movieHistoryApi->fetchLastPlays($userId); - } else if($row->isMostWatchedActors()) { - $response['mostWatchedActors'] = $this->movieHistoryApi->fetchActors($userId, 6, 1, gender: Gender::createMale(), personFilterUserId: $currentUserId); - } else if($row->isMostWatchedActresses()) { - $response['mostWatchedActresses'] = $this->movieHistoryApi->fetchActors($userId, 6, 1, gender: Gender::createFemale(), personFilterUserId: $currentUserId); - } else if($row->isMostWatchedDirectors()) { - $response['mostWatchedDirectors'] = $this->movieHistoryApi->fetchDirectors($userId, 6, 1, personFilterUserId: $currentUserId); - } else if($row->isMostWatchedLanguages()) { + } elseif($row->isMostWatchedActors()) { + $response['mostWatchedActors'] = $this->movieHistoryApi->fetchActors($userId, 6, 1, gender: Gender::createMale(), personFilterUserId: $userId); + } elseif($row->isMostWatchedActresses()) { + $response['mostWatchedActresses'] = $this->movieHistoryApi->fetchActors($userId, 6, 1, gender: Gender::createFemale(), personFilterUserId: $userId); + } elseif($row->isMostWatchedDirectors()) { + $response['mostWatchedDirectors'] = $this->movieHistoryApi->fetchDirectors($userId, 6, 1, personFilterUserId: $userId); + } elseif($row->isMostWatchedLanguages()) { $response['mostWatchedLanguages'] = $this->movieHistoryApi->fetchMostWatchedLanguages($userId); - } else if($row->isMostWatchedGenres()) { + } elseif($row->isMostWatchedGenres()) { $response['mostWatchedGenres'] = $this->movieHistoryApi->fetchMostWatchedGenres($userId); - } else if($row->isMostWatchedProductionCompanies()) { + } elseif($row->isMostWatchedProductionCompanies()) { $response['mostWatchedProductionCompanies'] = $this->movieHistoryApi->fetchMostWatchedProductionCompanies($userId, 12); - } else if($row->isMostWatchedReleaseYears()) { + } elseif($row->isMostWatchedReleaseYears()) { $response['mostWatchedReleaseYears'] = $this->movieHistoryApi->fetchMostWatchedReleaseYears($userId); - } else if($row->isWatchlist()) { + } elseif($row->isWatchlist()) { $response['watchlistItems'] = $this->movieWatchlistApi->fetchWatchlistPaginated($userId, 6, 1); } } diff --git a/src/Service/Dashboard/Dto/DashboardRowList.php b/src/Service/Dashboard/Dto/DashboardRowList.php index 6195b8319..85831ee77 100644 --- a/src/Service/Dashboard/Dto/DashboardRowList.php +++ b/src/Service/Dashboard/Dto/DashboardRowList.php @@ -38,17 +38,13 @@ public function addAtOffset(int $position, DashboardRow $dashboardRow) : void public function asArray() : array { $serialized = []; - /** - * @var $row DashboardRow - * @var $this->data array - */ foreach($this->data as $row) { - array_push($serialized, [ + $serialized[] = [ 'row' => $row->getName(), 'id' => $row->getId(), 'isExtended' => $row->isExtended(), 'isVisible' => $row->isVisible() - ]); + ]; } return $serialized; } From 73aa4e0f5685774bedaafbce4f5f249fc80ce395 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Fri, 23 Feb 2024 15:05:54 +0100 Subject: [PATCH 07/21] Fix tests --- src/HttpController/Api/DashboardController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/HttpController/Api/DashboardController.php b/src/HttpController/Api/DashboardController.php index 997b2afe1..3497e05f6 100644 --- a/src/HttpController/Api/DashboardController.php +++ b/src/HttpController/Api/DashboardController.php @@ -26,6 +26,7 @@ public function __construct( ) { } + // phpcs:ignore public function getDashboardData(Request $request) : Response { $requestedUser = $this->userApi->findUserByName((string)$request->getRouteParameters()['username']); @@ -34,7 +35,7 @@ public function getDashboardData(Request $request) : Response } $userId = $requestedUser->getId(); - $dashboardRows = $this->dashboardFactory->createDashboardRowsForUser($this->userApi->fetchUser($userId)); + $dashboardRows = $this->dashboardFactory->createDashboardRowsForUser($requestedUser); $response = [ 'users' => $this->userPageAuthorizationChecker->fetchAllVisibleUsernamesForCurrentVisitor(), From 767bf8c9c99d7060d1c24320cb647b17430de318 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Fri, 23 Feb 2024 15:14:55 +0100 Subject: [PATCH 08/21] Fix tests --- src/HttpController/Api/MovieRatingController.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/HttpController/Api/MovieRatingController.php b/src/HttpController/Api/MovieRatingController.php index e82b0ff0a..267a59853 100644 --- a/src/HttpController/Api/MovieRatingController.php +++ b/src/HttpController/Api/MovieRatingController.php @@ -28,6 +28,9 @@ public function fetchMovieRatingByTmdbdId(Request $request) : Response $userRating = null; $movie = $this->movieApi->findByTmdbId((int)$tmdbId); + if($userId === null) { + return Response::createForbidden(); + } if ($movie !== null) { $userRating = $this->movieApi->findUserRating($movie->getId(), $userId); } @@ -40,6 +43,9 @@ public function fetchMovieRatingByTmdbdId(Request $request) : Response public function updateRating(Request $request) : Response { $userId = $this->authenticationService->getUserIdByApiToken($request); + if($userId === null) { + return Response::createForbidden(); + } if ($this->userApi->fetchUser($userId)->getName() !== $request->getRouteParameters()['username']) { return Response::createForbidden(); From 720692c7a1128cd375fb79bcd5f2f33b478058de Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Sat, 24 Feb 2024 17:08:36 +0100 Subject: [PATCH 09/21] Fix OpenAPI spec --- docs/openapi.json | 109 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/docs/openapi.json b/docs/openapi.json index 964a35b61..053cef822 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1066,6 +1066,115 @@ } } }, + "/users/{username}/movies/{id}/rating": { + "post": { + "tags": [ + "Rating" + ], + "summary": "Update the rating of an user for a movie", + "description": "Update the rating of an user for a movie", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "Movary ID of the movie", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application\/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "rating": { + "type": "integer", + "description": "Rating of the movie. It has to be an integer in the range of 0 - 10. If the rating is 0, then the rating will be deleted.", + "example": 10, + "minimum": 0, + "maximum": 10 + } + } + } + } + } + } + }, + "responses": { + "204": { + "$ref": "#/components/responses/204" + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "$ref": "#/components/responses/404" + } + }, + "security": [ + { + "authToken": [], + "cookieauth": [] + } + ] + } + }, + "\/fetchMovieRatingByTmdbdId": { + "get": { + "tags": [ + "Rating" + ], + "summary": "Get movie rating", + "description": "Get the movie rating from the current user. The movie is found by using the TMDB ID", + "parameters": [ + { + "name": "tmdbId", + "description": "The ID of the movie from TMDB", + "in": "query", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "The TMDB ID was valid and the ratings have been returned.", + "content": { + "application\/json": { + "schema": { + "type": "object", + "properties": { + "personalRating": { + "type": "integer", + "description": "The rating of the movie by the user. It will be null if there is no rating.", + "minimum": 1, + "maximum": 10 + } + } + } + } + } + }, + "403": { + "$ref": "#/components/responses/403" + } + } + } + }, "/authentication/token": { "post": { "tags": [ From b02461d20d83a77623025d763954065164eea2f5 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Mon, 26 Feb 2024 13:57:34 +0100 Subject: [PATCH 10/21] Add API endpoint to create a new user --- bootstrap.php | 3 +- docs/openapi.json | 94 ++++++++++++-- public/js/createnewuser.js | 27 ++++ settings/routes.php | 9 +- src/Domain/User/Service/Authentication.php | 2 +- src/Domain/User/Service/Validator.php | 3 + src/Domain/User/UserApi.php | 4 + src/Factory.php | 19 ++- .../Api/CreateUserController.php | 116 ++++++++++++++++++ .../Api/Middleware/CreateUserMiddleware.php | 25 ++++ .../Api/Middleware/IsUnauthenticated.php | 24 ++++ .../Web/AuthenticationController.php | 4 +- .../Web/CreateUserController.php | 93 -------------- .../ServerHasRegistrationEnabled.php | 22 ---- .../Web/Middleware/ServerHasUsers.php | 23 ---- templates/page/create-user.html.twig | 56 +++------ 16 files changed, 324 insertions(+), 200 deletions(-) create mode 100644 public/js/createnewuser.js create mode 100644 src/HttpController/Api/CreateUserController.php create mode 100644 src/HttpController/Api/Middleware/CreateUserMiddleware.php create mode 100644 src/HttpController/Api/Middleware/IsUnauthenticated.php delete mode 100644 src/HttpController/Web/Middleware/ServerHasRegistrationEnabled.php delete mode 100644 src/HttpController/Web/Middleware/ServerHasUsers.php diff --git a/bootstrap.php b/bootstrap.php index 6fb46d622..50ea805ec 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -7,6 +7,7 @@ $builder = new DI\ContainerBuilder(); $builder->addDefinitions( [ + \Movary\HttpController\Web\AuthenticationController::class => DI\Factory([Factory::class, 'createAuthenticationController']), \Movary\ValueObject\Config::class => DI\factory([Factory::class, 'createConfig']), \Movary\Api\Trakt\TraktApi::class => DI\factory([Factory::class, 'createTraktApi']), \Movary\Service\ImageCacheService::class => DI\factory([Factory::class, 'createImageCacheService']), @@ -18,7 +19,7 @@ \Movary\HttpController\Web\CreateUserController::class => DI\factory([Factory::class, 'createCreateUserController']), \Movary\HttpController\Web\JobController::class => DI\factory([Factory::class, 'createJobController']), \Movary\HttpController\Web\LandingPageController::class => DI\factory([Factory::class, 'createLandingPageController']), - \Movary\HttpController\Web\Middleware\ServerHasRegistrationEnabled::class => DI\factory([Factory::class, 'createMiddlewareServerHasRegistrationEnabled']), + \Movary\HttpController\Api\Middleware\CreateUserMiddleware::class => DI\factory([Factory::class, 'createUserMiddleware']), \Movary\ValueObject\Http\Request::class => DI\factory([Factory::class, 'createCurrentHttpRequest']), \Movary\Command\CreatePublicStorageLink::class => DI\factory([Factory::class, 'createCreatePublicStorageLink']), \Movary\Command\DatabaseMigrationStatus::class => DI\factory([Factory::class, 'createDatabaseMigrationStatusCommand']), diff --git a/docs/openapi.json b/docs/openapi.json index 964a35b61..692906b96 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1074,13 +1074,7 @@ "description": "Create an authentication token via email, password and optionally TOTP code. Add the token as X-Auth-Token header to further requests. Token lifetime 1d default, 30d with rememberMe.", "parameters": [ { - "in": "header", - "name": "X-Movary-Client", - "schema": { - "type": "string" - }, - "required": true, - "example": "Client Name" + "$ref": "#/components/parameters/deviceName" } ], "requestBody": { @@ -1152,6 +1146,81 @@ } } } + }, + "/create-user": { + "post": { + "tags": [ + "Account" + ], + "summary": "Create a new user", + "description": "Creates new user if one of the conditions are true: Public registration is enabled or the server has no users.", + "parameters": [ + { + "$ref": "#/components/parameters/deviceName" + } + ], + "requestBody": { + "description": "The request data in JSON format.", + "content": { + "application/json": { + "required": true, + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "myname@domain.com", + "description": "E-Mail address of the new user" + }, + "username": { + "type": "string", + "example": "A very cool username", + "description": "The username of the new user" + }, + "password": { + "type": "string", + "example": "My secure password", + "description": "The password of the new user" + }, + "repeatPassword": { + "type": "string", + "example": "My secure password", + "description": "The password of the new user but repeated for security." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Returned if the user was created.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "userId": { + "type": "integer", + "description": "The id of the newly created user." + }, + "token": { + "type": "string", + "description": "The authentication token to be used in future requests." + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "403": { + "$ref": "#/components/responses/403" + } + } + } } }, "components": { @@ -1369,6 +1438,17 @@ "description": "The resource was not found" } }, + "parameters": { + "deviceName": { + "in": "header", + "name": "X-Movary-Client", + "schema": { + "type": "string" + }, + "required": true, + "example": "Client Name" + } + }, "securitySchemes": { "token": { "type": "apiKey", diff --git a/public/js/createnewuser.js b/public/js/createnewuser.js new file mode 100644 index 000000000..fd9890361 --- /dev/null +++ b/public/js/createnewuser.js @@ -0,0 +1,27 @@ +const MOVARY_CLIENT_IDENTIFIER = 'Movary Web'; +const button = document.getElementById('createNewUserBtn'); + +async function submitNewUser() { + await fetch('/api/create-user', { + 'method': 'POST', + 'headers': { + 'Content-Type': 'application/json', + 'X-Movary-Client': MOVARY_CLIENT_IDENTIFIER + }, + 'body': JSON.stringify({ + "email": document.getElementById('emailInput').value, + "username": document.getElementById('usernameInput').value, + "password": document.getElementById('passwordInput').value, + "repeatPassword": document.getElementById('repeatPasswordInput').value + }), + }).then(response => { + if(response.status === 200) { + window.location.href = '/'; + } else { + return response.json(); + } + }).then(error => { + document.getElementById('createUserResponse').innerText = error['message']; + document.getElementById('createUserResponse').classList.remove('d-none'); + }); +} \ No newline at end of file diff --git a/settings/routes.php b/settings/routes.php index 6fcac1c1d..4a271a3a8 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -21,15 +21,9 @@ function addWebRoutes(RouterService $routerService, FastRoute\RouteCollector $ro $routes->add('GET', '/login', [Web\AuthenticationController::class, 'renderLoginPage'], [Web\Middleware\UserIsUnauthenticated::class]); $routes->add('POST', '/login', [Web\AuthenticationController::class, 'login']); $routes->add('GET', '/logout', [Web\AuthenticationController::class, 'logout']); - $routes->add('POST', '/create-user', [Web\CreateUserController::class, 'createUser'], [ - Web\Middleware\UserIsUnauthenticated::class, - Web\Middleware\ServerHasUsers::class, - Web\Middleware\ServerHasRegistrationEnabled::class - ]); $routes->add('GET', '/create-user', [Web\CreateUserController::class, 'renderPage'], [ Web\Middleware\UserIsUnauthenticated::class, - Web\Middleware\ServerHasUsers::class, - Web\Middleware\ServerHasRegistrationEnabled::class + Api\Middleware\CreateUserMiddleware::class, ]); $routes->add('GET', '/docs/api', [Web\OpenApiController::class, 'renderPage']); @@ -203,6 +197,7 @@ function addApiRoutes(RouterService $routerService, FastRoute\RouteCollector $ro $routes->add('GET', '/openapi', [Api\OpenApiController::class, 'getSchema']); $routes->add('POST', '/authentication/token', [Api\AuthenticationController::class, 'createToken']); + $routes->add('POST', '/create-user', [Api\CreateUserController::class, 'createUser'], [Api\Middleware\IsUnauthenticated::class, Api\Middleware\CreateUserMiddleware::class]); $routeUserHistory = '/users/{username:[a-zA-Z0-9]+}/history/movies'; $routes->add('GET', $routeUserHistory, [Api\HistoryController::class, 'getHistory'], [Api\Middleware\IsAuthorizedToReadUserData::class]); diff --git a/src/Domain/User/Service/Authentication.php b/src/Domain/User/Service/Authentication.php index c19639a0a..0cd63394a 100644 --- a/src/Domain/User/Service/Authentication.php +++ b/src/Domain/User/Service/Authentication.php @@ -9,7 +9,7 @@ use Movary\Domain\User\UserApi; use Movary\Domain\User\UserEntity; use Movary\Domain\User\UserRepository; -use Movary\HttpController\Web\CreateUserController; +use Movary\HttpController\Api\CreateUserController; use Movary\Util\SessionWrapper; use Movary\ValueObject\DateTime; use Movary\ValueObject\Http\Request; diff --git a/src/Domain/User/Service/Validator.php b/src/Domain/User/Service/Validator.php index 6061aa61b..602e4680c 100644 --- a/src/Domain/User/Service/Validator.php +++ b/src/Domain/User/Service/Validator.php @@ -48,6 +48,9 @@ public function ensureNameIsUnique(string $name, ?int $expectUserId = null) : vo } } + /** + * @throws PasswordTooShort + */ public function ensurePasswordIsValid(string $password) : void { if (strlen($password) < self::PASSWORD_MIN_LENGTH) { diff --git a/src/Domain/User/UserApi.php b/src/Domain/User/UserApi.php index a104666aa..f1b564528 100644 --- a/src/Domain/User/UserApi.php +++ b/src/Domain/User/UserApi.php @@ -6,6 +6,7 @@ use Movary\Api\Jellyfin\Dto\JellyfinAuthenticationData; use Movary\Api\Jellyfin\Dto\JellyfinUserId; use Movary\Api\Plex\Dto\PlexAccessToken; +use Movary\Domain\User\Exception\PasswordTooShort; use Movary\Domain\User\Service\Validator; use Movary\ValueObject\Url; use Ramsey\Uuid\Uuid; @@ -19,6 +20,9 @@ public function __construct( ) { } + /** + * @throws PasswordTooShort + */ public function createUser(string $email, string $password, string $name, bool $isAdmin = false) : void { $this->userValidator->ensureEmailIsUnique($email); diff --git a/src/Factory.php b/src/Factory.php index b59fce2ad..f5dc53433 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -13,7 +13,6 @@ use Movary\Api\Trakt\Cache\User\Movie\Watched; use Movary\Api\Trakt\TraktApi; use Movary\Api\Trakt\TraktClient; -use Movary\Command; use Movary\Command\CreatePublicStorageLink; use Movary\Domain\Movie\MovieApi; use Movary\Domain\Movie\Watchlist\MovieWatchlistApi; @@ -21,6 +20,7 @@ use Movary\Domain\User\Service\Authentication; use Movary\Domain\User\UserApi; use Movary\HttpController\Api\OpenApiController; +use Movary\HttpController\Web\AuthenticationController; use Movary\HttpController\Web\CreateUserController; use Movary\HttpController\Web\JobController; use Movary\HttpController\Web\LandingPageController; @@ -64,6 +64,16 @@ class Factory private const DEFAULT_ENABLE_FILE_LOGGING = true; + public static function createAuthenticationController(Config $config, ContainerInterface $container) : AuthenticationController + { + return new AuthenticationController( + $container->get(Twig\Environment::class), + $container->get(Authentication::class), + $container->get(SessionWrapper::class), + $config->getAsBool('ENABLE_REGISTRATION') + ); + } + public static function createConfig(ContainerInterface $container) : Config { $dotenv = Dotenv::createMutable(self::createDirectoryAppRoot()); @@ -91,9 +101,7 @@ public static function createCreateUserController(ContainerInterface $container) { return new CreateUserController( $container->get(Twig\Environment::class), - $container->get(Authentication::class), $container->get(UserApi::class), - $container->get(SessionWrapper::class), ); } @@ -240,9 +248,10 @@ public static function createLogger(ContainerInterface $container, Config $confi return $logger; } - public static function createMiddlewareServerHasRegistrationEnabled(Config $config) : HttpController\Web\Middleware\ServerHasRegistrationEnabled + public static function createUserMiddleware(Config $config, ContainerInterface $container) : HttpController\Api\Middleware\createUserMiddleware { - return new HttpController\Web\Middleware\ServerHasRegistrationEnabled( + return new HttpController\Api\Middleware\CreateUserMiddleware( + $container->get(UserApi::class), $config->getAsBool('ENABLE_REGISTRATION', false) ); } diff --git a/src/HttpController/Api/CreateUserController.php b/src/HttpController/Api/CreateUserController.php new file mode 100644 index 000000000..20aa24715 --- /dev/null +++ b/src/HttpController/Api/CreateUserController.php @@ -0,0 +1,116 @@ +userApi->hasUsers(); + $jsonData = Json::decode($request->getBody()); + + $deviceName = $request->getHeaders()['X-Movary-Client'] ?? null; + if(empty($deviceName)) { + return Response::createBadRequest('No client header'); + } + $userAgent = $request->getUserAgent(); + + $email = empty($jsonData['email']) === true ? null : (string)$jsonData['email']; + $username = empty($jsonData['username']) === true ? null : (string)$jsonData['username']; + $password = empty($jsonData['password']) === true ? null : (string)$jsonData['password']; + $repeatPassword = empty($jsonData['repeatPassword']) === true ? null : (string)$jsonData['repeatPassword']; + + if ($email === null || $username === null || $password === null || $repeatPassword === null) { + return Response::createBadRequest( + Json::encode([ + 'error' => 'MissingInput', + 'message' => 'Email, username, password or the password repeat is missing' + ]), + [Header::createContentTypeJson()], + ); + } + + if ($password !== $repeatPassword) { + return Response::createBadRequest( + Json::encode([ + 'error' => 'PasswordsNotEqual', + 'message' => 'The repeated password is not the same as the password' + ]), + [Header::createContentTypeJson()], + ); + } + + try { + $this->userApi->createUser($email, $password, $username, $hasUsers === false); + $userAndAuthToken = $this->authenticationService->login($email, $password, false, $deviceName, $userAgent); + + return Response::createJson( + Json::encode([ + 'userId' => $userAndAuthToken['user']->getId(), + 'authToken' => $userAndAuthToken['token'] + ]), + ); + } catch (UsernameInvalidFormat) { + return Response::createBadRequest( + Json::encode([ + 'error' => 'UsernameInvalidFormat', + 'message' => 'Username can only contain letters or numbers' + ]), + [Header::createContentTypeJson()], + ); + } catch (UsernameNotUnique) { + return Response::createBadRequest( + Json::encode([ + 'error' => 'UsernameNotUnique', + 'message' => 'Username is already taken' + ]), + [Header::createContentTypeJson()], + ); + } catch (EmailNotUnique) { + return Response::createBadRequest( + Json::encode([ + 'error' => 'EmailNotUnique', + 'message' => 'Email is already taken' + ]), + [Header::createContentTypeJson()], + ); + } catch(PasswordTooShort) { + return Response::createBadRequest( + Json::encode([ + 'error' => 'PasswordTooShort', + 'message' => 'Password must be at least 8 characters' + ]), + [Header::createContentTypeJson()], + ); + } catch (Throwable) { + return Response::createBadRequest( + Json::encode([ + 'error' => 'GenericError', + 'message' => 'Something has gone wrong. Please check the logs and try again later.' + ]), + [Header::createContentTypeJson()], + ); + } + } +} \ No newline at end of file diff --git a/src/HttpController/Api/Middleware/CreateUserMiddleware.php b/src/HttpController/Api/Middleware/CreateUserMiddleware.php new file mode 100644 index 000000000..61f77e9d4 --- /dev/null +++ b/src/HttpController/Api/Middleware/CreateUserMiddleware.php @@ -0,0 +1,25 @@ +registrationEnabled === false && $this->userApi->hasUsers() === true) { + return Response::createForbidden(); + } + + return null; + } +} diff --git a/src/HttpController/Api/Middleware/IsUnauthenticated.php b/src/HttpController/Api/Middleware/IsUnauthenticated.php new file mode 100644 index 000000000..47f39ea6f --- /dev/null +++ b/src/HttpController/Api/Middleware/IsUnauthenticated.php @@ -0,0 +1,24 @@ +authenticationService->getUserIdByApiToken($request) !== null) { + return Response::createForbidden(); + } + + return null; + } +} diff --git a/src/HttpController/Web/AuthenticationController.php b/src/HttpController/Web/AuthenticationController.php index e08487598..8d318ccb5 100644 --- a/src/HttpController/Web/AuthenticationController.php +++ b/src/HttpController/Web/AuthenticationController.php @@ -20,6 +20,7 @@ public function __construct( private readonly Environment $twig, private readonly Service\Authentication $authenticationService, private readonly SessionWrapper $sessionWrapper, + private readonly bool $registrationEnabled ) { } @@ -44,7 +45,8 @@ public function renderLoginPage(Request $request) : Response 'page/login.html.twig', [ 'failedLogin' => $failedLogin, - 'redirect' => $redirect + 'redirect' => $redirect, + 'registrationEnabled' => $this->registrationEnabled ], ); diff --git a/src/HttpController/Web/CreateUserController.php b/src/HttpController/Web/CreateUserController.php index 1af6d6dd4..cedf8e17c 100644 --- a/src/HttpController/Web/CreateUserController.php +++ b/src/HttpController/Web/CreateUserController.php @@ -2,120 +2,27 @@ namespace Movary\HttpController\Web; -use Movary\Domain\User\Exception\EmailNotUnique; -use Movary\Domain\User\Exception\PasswordTooShort; -use Movary\Domain\User\Exception\UsernameInvalidFormat; -use Movary\Domain\User\Exception\UsernameNotUnique; -use Movary\Domain\User\Service\Authentication; use Movary\Domain\User\UserApi; -use Movary\Util\SessionWrapper; -use Movary\ValueObject\Http\Header; -use Movary\ValueObject\Http\Request; use Movary\ValueObject\Http\Response; use Movary\ValueObject\Http\StatusCode; -use Throwable; use Twig\Environment; class CreateUserController { - public const MOVARY_WEB_CLIENT = 'Movary Web'; - public function __construct( private readonly Environment $twig, - private readonly Authentication $authenticationService, private readonly UserApi $userApi, - private readonly SessionWrapper $sessionWrapper, ) { } - // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh - public function createUser(Request $request) : Response - { - $hasUsers = $this->userApi->hasUsers(); - $postParameters = $request->getPostParameters(); - - $userAgent = $request->getUserAgent(); - $email = empty($postParameters['email']) === true ? null : (string)$postParameters['email']; - $name = empty($postParameters['name']) === true ? null : (string)$postParameters['name']; - $password = empty($postParameters['password']) === true ? null : (string)$postParameters['password']; - $repeatPassword = empty($postParameters['password']) === true ? null : (string)$postParameters['repeatPassword']; - - if ($email === null || $name === null || $password === null || $repeatPassword === null) { - $this->sessionWrapper->set('missingFormData', true); - - return Response::create( - StatusCode::createSeeOther(), - null, - [Header::createLocation($_SERVER['HTTP_REFERER'])], - ); - } - - if ($password !== $repeatPassword) { - $this->sessionWrapper->set('errorPasswordNotEqual', true); - - return Response::create( - StatusCode::createSeeOther(), - null, - [Header::createLocation($_SERVER['HTTP_REFERER'])], - ); - } - - try { - $this->userApi->createUser($email, $password, $name, $hasUsers === false); - - $this->authenticationService->login($email, $password, false, self::MOVARY_WEB_CLIENT, $userAgent); - } catch (PasswordTooShort) { - $this->sessionWrapper->set('errorPasswordTooShort', true); - } catch (UsernameInvalidFormat) { - $this->sessionWrapper->set('errorUsernameInvalidFormat', true); - } catch (UsernameNotUnique) { - $this->sessionWrapper->set('errorUsernameUnique', true); - } catch (EmailNotUnique) { - $this->sessionWrapper->set('errorEmailUnique', true); - } catch (Throwable) { - $this->sessionWrapper->set('errorGeneric', true); - } - - return Response::create( - StatusCode::createSeeOther(), - null, - [Header::createLocation($_SERVER['HTTP_REFERER'])], - ); - } - public function renderPage() : Response { $hasUsers = $this->userApi->hasUsers(); - $errorPasswordTooShort = $this->sessionWrapper->find('errorPasswordTooShort'); - $errorPasswordNotEqual = $this->sessionWrapper->find('errorPasswordNotEqual'); - $errorUsernameInvalidFormat = $this->sessionWrapper->find('errorUsernameInvalidFormat'); - $errorUsernameUnique = $this->sessionWrapper->find('errorUsernameUnique'); - $errorEmailUnique = $this->sessionWrapper->find('errorEmailUnique'); - $missingFormData = $this->sessionWrapper->find('missingFormData'); - $errorGeneric = $this->sessionWrapper->find('errorGeneric'); - - $this->sessionWrapper->unset( - 'errorPasswordTooShort', - 'errorPasswordNotEqual', - 'errorUsernameInvalidFormat', - 'errorUsernameUnique', - 'errorEmailUnique', - 'errorGeneric', - 'missingFormData', - ); - return Response::create( StatusCode::createOk(), $this->twig->render('page/create-user.html.twig', [ 'subtitle' => $hasUsers === false ? 'Create initial admin user' : 'Create new user', - 'errorPasswordTooShort' => $errorPasswordTooShort, - 'errorPasswordNotEqual' => $errorPasswordNotEqual, - 'errorUsernameInvalidFormat' => $errorUsernameInvalidFormat, - 'errorUsernameUnique' => $errorUsernameUnique, - 'errorEmailUnique' => $errorEmailUnique, - 'errorGeneric' => $errorGeneric, - 'missingFormData' => $missingFormData ]), ); } diff --git a/src/HttpController/Web/Middleware/ServerHasRegistrationEnabled.php b/src/HttpController/Web/Middleware/ServerHasRegistrationEnabled.php deleted file mode 100644 index 5c2f8cc5c..000000000 --- a/src/HttpController/Web/Middleware/ServerHasRegistrationEnabled.php +++ /dev/null @@ -1,22 +0,0 @@ -registrationEnabled === false) { - return null; - } - - return Response::createForbidden(); - } -} diff --git a/src/HttpController/Web/Middleware/ServerHasUsers.php b/src/HttpController/Web/Middleware/ServerHasUsers.php deleted file mode 100644 index 8a9772671..000000000 --- a/src/HttpController/Web/Middleware/ServerHasUsers.php +++ /dev/null @@ -1,23 +0,0 @@ -userApi->hasUsers() === false) { - return null; - } - - return Response::createSeeOther('/'); - } -} diff --git a/templates/page/create-user.html.twig b/templates/page/create-user.html.twig index 4ca506e92..8f9d522b5 100644 --- a/templates/page/create-user.html.twig +++ b/templates/page/create-user.html.twig @@ -8,61 +8,37 @@ {% endblock %} +{% block scripts %} + +{% endblock %} + {% block body %} - + Movary {{ subtitle }} - - Email address * + + Email address * - - Username * + + Username * - - Password * + + Password * - - Repeat password * + + Repeat password * - {% if errorPasswordNotEqual == true %} - - Passwords not equal - - {% endif %} - {% if errorPasswordTooShort == true %} - - Password must be at least 8 characters - - {% endif %} - {% if errorUsernameInvalidFormat == true %} - - Username must consist of only letters and numbers - - {% endif %} - {% if errorUsernameUnique == true %} - - Username is already used - - {% endif %} - {% if errorEmailUnique == true %} - - Email is already used - - {% endif %} - {% if missingFormData == true %} - - Please fill out all fields - - {% endif %} - Create + + Create + Return to login {% endblock %} From 37ee3f9667882ed50e15228b4905155b1d5a72cf Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:02:11 +0100 Subject: [PATCH 11/21] Fix tests --- bootstrap.php | 2 +- src/Domain/User/UserApi.php | 4 ---- src/Factory.php | 2 +- src/HttpController/Api/CreateUserController.php | 1 - 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/bootstrap.php b/bootstrap.php index 50ea805ec..5c524386e 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -19,7 +19,7 @@ \Movary\HttpController\Web\CreateUserController::class => DI\factory([Factory::class, 'createCreateUserController']), \Movary\HttpController\Web\JobController::class => DI\factory([Factory::class, 'createJobController']), \Movary\HttpController\Web\LandingPageController::class => DI\factory([Factory::class, 'createLandingPageController']), - \Movary\HttpController\Api\Middleware\CreateUserMiddleware::class => DI\factory([Factory::class, 'createUserMiddleware']), + \Movary\HttpController\Api\Middleware\CreateUserMiddleware::class => DI\factory([Factory::class, 'createCreateUserMiddleware']), \Movary\ValueObject\Http\Request::class => DI\factory([Factory::class, 'createCurrentHttpRequest']), \Movary\Command\CreatePublicStorageLink::class => DI\factory([Factory::class, 'createCreatePublicStorageLink']), \Movary\Command\DatabaseMigrationStatus::class => DI\factory([Factory::class, 'createDatabaseMigrationStatusCommand']), diff --git a/src/Domain/User/UserApi.php b/src/Domain/User/UserApi.php index f1b564528..a104666aa 100644 --- a/src/Domain/User/UserApi.php +++ b/src/Domain/User/UserApi.php @@ -6,7 +6,6 @@ use Movary\Api\Jellyfin\Dto\JellyfinAuthenticationData; use Movary\Api\Jellyfin\Dto\JellyfinUserId; use Movary\Api\Plex\Dto\PlexAccessToken; -use Movary\Domain\User\Exception\PasswordTooShort; use Movary\Domain\User\Service\Validator; use Movary\ValueObject\Url; use Ramsey\Uuid\Uuid; @@ -20,9 +19,6 @@ public function __construct( ) { } - /** - * @throws PasswordTooShort - */ public function createUser(string $email, string $password, string $name, bool $isAdmin = false) : void { $this->userValidator->ensureEmailIsUnique($email); diff --git a/src/Factory.php b/src/Factory.php index f5dc53433..9d0b031e4 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -248,7 +248,7 @@ public static function createLogger(ContainerInterface $container, Config $confi return $logger; } - public static function createUserMiddleware(Config $config, ContainerInterface $container) : HttpController\Api\Middleware\createUserMiddleware + public static function createCreateUserMiddleware(Config $config, ContainerInterface $container) : HttpController\Api\Middleware\CreateUserMiddleware { return new HttpController\Api\Middleware\CreateUserMiddleware( $container->get(UserApi::class), diff --git a/src/HttpController/Api/CreateUserController.php b/src/HttpController/Api/CreateUserController.php index 20aa24715..cc5679413 100644 --- a/src/HttpController/Api/CreateUserController.php +++ b/src/HttpController/Api/CreateUserController.php @@ -9,7 +9,6 @@ use Movary\Domain\User\Service\Authentication; use Movary\Domain\User\UserApi; use Movary\Util\Json; -use Movary\Util\SessionWrapper; use Movary\ValueObject\Http\Header; use Movary\ValueObject\Http\Request; use Movary\ValueObject\Http\Response; From 0f187d7be7e3369ef6c09e5c9e9c92bcb36c234c Mon Sep 17 00:00:00 2001 From: JVT038 Date: Mon, 26 Feb 2024 17:30:25 +0100 Subject: [PATCH 12/21] Add HTTP tests Signed-off-by: JVT038 <47184046+JVT038@users.noreply.github.com> --- .../Api/CreateUserController.php | 4 +- tests/rest/api/create-user.http | 180 ++++++++++++++++++ 2 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 tests/rest/api/create-user.http diff --git a/src/HttpController/Api/CreateUserController.php b/src/HttpController/Api/CreateUserController.php index cc5679413..7c092a67a 100644 --- a/src/HttpController/Api/CreateUserController.php +++ b/src/HttpController/Api/CreateUserController.php @@ -12,7 +12,7 @@ use Movary\ValueObject\Http\Header; use Movary\ValueObject\Http\Request; use Movary\ValueObject\Http\Response; -use Throwable; +use Exception; class CreateUserController { @@ -102,7 +102,7 @@ public function createUser(Request $request) : Response ]), [Header::createContentTypeJson()], ); - } catch (Throwable) { + } catch (Exception) { return Response::createBadRequest( Json::encode([ 'error' => 'GenericError', diff --git a/tests/rest/api/create-user.http b/tests/rest/api/create-user.http new file mode 100644 index 000000000..41f643b83 --- /dev/null +++ b/tests/rest/api/create-user.http @@ -0,0 +1,180 @@ +POST http://127.0.0.1/api/create-user +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: RestAPI Test + +{ + "email": "myname@domain.com", + "username": "Averycoolusername", + "password": "Mysecurepassword", + "repeatPassword": "Mysecurepassword" +} + +> {% + client.test("User created", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); +%} + +### + +POST http://127.0.0.1/api/create-user +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: RestAPI Test + +{ + "email": "", + "username": "", + "password": "", + "repeatPassword": "" +} + +> {% + client.test("Missing input", function() { + client.assert(response.status === 400, "Response status is not 200"); + client.assert(response.body['error'] === 'MissingInput', 'Response error was not as expected'); + client.assert(response.body['message'] === 'Email, username, password or the password repeat is missing', 'Response was not as expected'); + }); +%} + +### + +POST http://127.0.0.1/api/create-user +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: RestAPI Test + +{ + "email": "myname@domain.com", + "username": "Averycoolusername", + "password": "Mysecurepassword", + "repeatPassword": "Adifferentpassword" +} + +> {% + client.test("Missing input", function() { + client.assert(response.status === 400, "Response status is not 200"); + client.assert(response.body['error'] === 'PasswordsNotEqual', 'Response error was not as expected'); + client.assert(response.body['message'] === 'The repeated password is not the same as the password', 'Response was not as expected'); + }); +%} + +### + +POST http://127.0.0.1/api/create-user +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: RestAPI Test + +{ + "email": "myname@domain.com", + "username": "MyUniqueUsername", + "password": "Mysecurepassword", + "repeatPassword": "Mysecurepassword" +} + +> {% + client.test("Missing input", function() { + client.assert(response.status === 400, "Response status is not 200"); + client.assert(response.body['error'] === 'EmailNotUnique', 'Response error was not MissingInput'); + client.assert(response.body['message'] === 'Email is already taken', 'Response was not as expected'); + }); +%} + +### + +POST http://127.0.0.1/api/create-user +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: RestAPI Test + +{ + "email": "myuniqueaddress@domain.com", + "username": "MyUniqueUsername", + "password": "short", + "repeatPassword": "short" +} + +> {% + client.test("Missing input", function() { + client.assert(response.status === 400, "Response status is not 200"); + client.assert(response.body['error'] === 'PasswordTooShort', 'Response error was not MissingInput'); + client.assert(response.body['message'] === 'Password must be at least 8 characters', 'Response was not as expected'); + }); +%} + +### + +POST http://127.0.0.1/api/create-user +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: RestAPI Test + +{ + "email": "myuniqueaddress@domain.com", + "username": "An invalid username!!!!", + "password": "Mysecurepassword", + "repeatPassword": "Mysecurepassword" +} + +> {% + client.test("Missing input", function() { + client.assert(response.status === 400, "Response status is not 200"); + client.assert(response.body['error'] === 'UsernameInvalidFormat', 'Response error was not as expected'); + client.assert(response.body['message'] === 'Username can only contain letters or numbers', 'Response was not as expected'); + }); +%} + +### + +POST http://127.0.0.1/api/create-user +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: RestAPI Test + +{ + "email": "myuniqueaddress@domain.com", + "username": "Averycoolusername", + "password": "Mysecurepassword", + "repeatPassword": "Mysecurepassword" +} + +> {% + client.test("Missing input", function() { + client.assert(response.status === 400, "Response status is not 200"); + client.assert(response.body['error'] === 'UsernameNotUnique', 'Response error was not as expected'); + client.assert(response.body['message'] === 'Username is already taken', 'Response was not as expected'); + }); +%} + +### + +POST http://127.0.0.1/api/create-user +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: RestAPI Test + +{ + "email": "myuniqueaddress@domain.com", + "username": "MyUniqueUsername", + "password": "short", + "repeatPassword": "short" +} + +> {% + client.test("Missing input", function() { + client.assert(response.status === 400, "Response status is not 200"); + client.assert(response.body['error'] === 'PasswordTooShort', 'Response error was not MissingInput'); + client.assert(response.body['message'] === 'Password must be at least 8 characters', 'Response was not as expected'); + }); +%} + From 5c0f196b2d9659efc267b834136ace1fc9f606ce Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Tue, 27 Feb 2024 20:39:04 +0100 Subject: [PATCH 13/21] Fix tests Signed-off-by: JVT038 <47184046+JVT038@users.noreply.github.com> --- tests/rest/api/create-user.http | 80 ++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/tests/rest/api/create-user.http b/tests/rest/api/create-user.http index 41f643b83..009e5da90 100644 --- a/tests/rest/api/create-user.http +++ b/tests/rest/api/create-user.http @@ -1,3 +1,4 @@ +#@name Test creating a new user properly POST http://127.0.0.1/api/create-user Accept: */* Cache-Control: no-cache @@ -15,9 +16,11 @@ X-Movary-Client: RestAPI Test client.test("User created", function() { client.assert(response.status === 200, "Response status is not 200"); }); + client.global.set('responseAuthToken', response.body.token); %} ### +#@name Test missing input erro POST http://127.0.0.1/api/create-user Accept: */* @@ -34,14 +37,16 @@ X-Movary-Client: RestAPI Test > {% client.test("Missing input", function() { - client.assert(response.status === 400, "Response status is not 200"); - client.assert(response.body['error'] === 'MissingInput', 'Response error was not as expected'); + let expectedStatusCode = 400; + let expectedError = 'MissingInput' + client.assert(response.status === expectedStatusCode, "Response status is not: " + expectedStatusCode); + client.assert(response.body['error'] === expectedError, 'Response error was not: ' + expectedError); client.assert(response.body['message'] === 'Email, username, password or the password repeat is missing', 'Response was not as expected'); }); %} ### - +#@name Test passwords not equal error POST http://127.0.0.1/api/create-user Accept: */* Cache-Control: no-cache @@ -56,14 +61,16 @@ X-Movary-Client: RestAPI Test } > {% - client.test("Missing input", function() { - client.assert(response.status === 400, "Response status is not 200"); - client.assert(response.body['error'] === 'PasswordsNotEqual', 'Response error was not as expected'); - client.assert(response.body['message'] === 'The repeated password is not the same as the password', 'Response was not as expected'); + client.test("Passwords not equal", function() { + let expectedStatusCode = 400; + let expectedError = 'PasswordsNotEqual' + client.assert(response.status === expectedStatusCode, "Response status is not: " + expectedStatusCode); + client.assert(response.body['error'] === expectedError, 'Response error was not: ' + expectedError); }); %} ### +#@name Test email already exists error POST http://127.0.0.1/api/create-user Accept: */* @@ -79,14 +86,16 @@ X-Movary-Client: RestAPI Test } > {% - client.test("Missing input", function() { - client.assert(response.status === 400, "Response status is not 200"); - client.assert(response.body['error'] === 'EmailNotUnique', 'Response error was not MissingInput'); - client.assert(response.body['message'] === 'Email is already taken', 'Response was not as expected'); + client.test("Email already exists", function() { + let expectedStatusCode = 400; + let expectedError = 'EmailNotUnique' + client.assert(response.status === expectedStatusCode, "Response status is not: " + expectedStatusCode); + client.assert(response.body['error'] === expectedError, 'Response error was not: ' + expectedError); }); %} ### +#@name Test password is shorter than 8 characters POST http://127.0.0.1/api/create-user Accept: */* @@ -102,14 +111,16 @@ X-Movary-Client: RestAPI Test } > {% - client.test("Missing input", function() { - client.assert(response.status === 400, "Response status is not 200"); - client.assert(response.body['error'] === 'PasswordTooShort', 'Response error was not MissingInput'); - client.assert(response.body['message'] === 'Password must be at least 8 characters', 'Response was not as expected'); + client.test("Password too short", function() { + let expectedStatusCode = 400; + let expectedError = 'PasswordTooShort' + client.assert(response.status === expectedStatusCode, "Response status is not: " + expectedStatusCode); + client.assert(response.body['error'] === expectedError, 'Response error was not: ' + expectedError); }); %} ### +#@name Test invalid username error POST http://127.0.0.1/api/create-user Accept: */* @@ -125,14 +136,16 @@ X-Movary-Client: RestAPI Test } > {% - client.test("Missing input", function() { - client.assert(response.status === 400, "Response status is not 200"); - client.assert(response.body['error'] === 'UsernameInvalidFormat', 'Response error was not as expected'); - client.assert(response.body['message'] === 'Username can only contain letters or numbers', 'Response was not as expected'); + client.test("Invalid username", function() { + let expectedStatusCode = 400; + let expectedError = 'UsernameInvalidFormat' + client.assert(response.status === expectedStatusCode, "Response status is not: " + expectedStatusCode); + client.assert(response.body['error'] === expectedError, 'Response error was not: ' + expectedError); }); %} ### +#@name Test username already exists error POST http://127.0.0.1/api/create-user Accept: */* @@ -148,33 +161,26 @@ X-Movary-Client: RestAPI Test } > {% - client.test("Missing input", function() { - client.assert(response.status === 400, "Response status is not 200"); - client.assert(response.body['error'] === 'UsernameNotUnique', 'Response error was not as expected'); - client.assert(response.body['message'] === 'Username is already taken', 'Response was not as expected'); + client.test("Username already exists", function() { + let expectedStatusCode = 400; + let expectedError = 'UsernameNotUnique' + client.assert(response.status === expectedStatusCode, "Response status is not: " + expectedStatusCode); + client.assert(response.body['error'] === expectedError, 'Response error was not: ' + expectedError); }); %} ### +#@name Delete the test user since all tests are finished -POST http://127.0.0.1/api/create-user +DELETE http://127.0.0.1/api/authentication/token Accept: */* Cache-Control: no-cache Content-Type: application/json -X-Movary-Client: RestAPI Test - -{ - "email": "myuniqueaddress@domain.com", - "username": "MyUniqueUsername", - "password": "short", - "repeatPassword": "short" -} +X-Auth-Token: {{responseAuthToken}} > {% - client.test("Missing input", function() { - client.assert(response.status === 400, "Response status is not 200"); - client.assert(response.body['error'] === 'PasswordTooShort', 'Response error was not MissingInput'); - client.assert(response.body['message'] === 'Password must be at least 8 characters', 'Response was not as expected'); + client.test("Response has correct status code", function() { + let expected = 204 + client.assert(response.status === expected, "Expected status code: " + expected); }); %} - From f589c70fc0eb76d5eb6b297feeb8929fe3ed12c7 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Tue, 27 Feb 2024 20:41:21 +0100 Subject: [PATCH 14/21] Fix tests Signed-off-by: JVT038 <47184046+JVT038@users.noreply.github.com> --- src/HttpController/Api/Middleware/CreateUserMiddleware.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/HttpController/Api/Middleware/CreateUserMiddleware.php b/src/HttpController/Api/Middleware/CreateUserMiddleware.php index 61f77e9d4..15b302c75 100644 --- a/src/HttpController/Api/Middleware/CreateUserMiddleware.php +++ b/src/HttpController/Api/Middleware/CreateUserMiddleware.php @@ -6,11 +6,11 @@ use Movary\HttpController\Web\Middleware\MiddlewareInterface; use Movary\ValueObject\Http\Response; -readonly class CreateUserMiddleware implements MiddlewareInterface +class CreateUserMiddleware implements MiddlewareInterface { public function __construct( - private UserApi $userApi, - private bool $registrationEnabled + readonly private UserApi $userApi, + readonly private bool $registrationEnabled ) { } From 46ba46b3735d517780ecaac83743e30b961345a2 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Tue, 27 Feb 2024 20:58:13 +0100 Subject: [PATCH 15/21] Add newline to fix test Signed-off-by: JVT038 <47184046+JVT038@users.noreply.github.com> --- src/HttpController/Api/CreateUserController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HttpController/Api/CreateUserController.php b/src/HttpController/Api/CreateUserController.php index 7c092a67a..e5478f26e 100644 --- a/src/HttpController/Api/CreateUserController.php +++ b/src/HttpController/Api/CreateUserController.php @@ -112,4 +112,4 @@ public function createUser(Request $request) : Response ); } } -} \ No newline at end of file +} From 9aa2e6bce2fd83bbe98f1b9b99d5f6bcddfeb9a7 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Tue, 27 Feb 2024 21:02:14 +0100 Subject: [PATCH 16/21] Fix phpstan test Signed-off-by: JVT038 <47184046+JVT038@users.noreply.github.com> --- src/Factory.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Factory.php b/src/Factory.php index c7bf1ffb1..3336bcde3 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -64,13 +64,12 @@ class Factory private const DEFAULT_ENABLE_FILE_LOGGING = true; - public static function createAuthenticationController(Config $config, ContainerInterface $container) : AuthenticationController + public static function createAuthenticationController(ContainerInterface $container) : AuthenticationController { return new AuthenticationController( $container->get(Twig\Environment::class), $container->get(Authentication::class), - $container->get(SessionWrapper::class), - $config->getAsBool('ENABLE_REGISTRATION') + $container->get(SessionWrapper::class) ); } From 823e5478012809a06aa6b239737405443f757c18 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:54:56 +0100 Subject: [PATCH 17/21] Added API endpoints to retrieve individual statistics. Also added HTTP tests and updated the OpenAPI specs Signed-off-by: JVT038 <47184046+JVT038@users.noreply.github.com> --- docs/openapi.json | 639 +++++++++++++++--- settings/routes.php | 3 +- ...ontroller.php => StatisticsController.php} | 59 +- tests/rest/api/user-dashboard.http | 5 - tests/rest/api/user-statistics.http | 78 +++ 5 files changed, 680 insertions(+), 104 deletions(-) rename src/HttpController/Api/{DashboardController.php => StatisticsController.php} (61%) delete mode 100644 tests/rest/api/user-dashboard.http create mode 100644 tests/rest/api/user-statistics.http diff --git a/docs/openapi.json b/docs/openapi.json index 1c6c940b4..c8cd31ca0 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -6,10 +6,10 @@ }, "servers": [], "paths": { - "\/users\/{username}\/dashboard": { + "\/users\/{username}\/statistics\/dashboard": { "get": { "tags": [ - "Dashboard" + "Statistics" ], "summary": "Get all the data that's shown in the dashboard", "description": "Get all the statistics and the order of the rows in the dashboard. When a row is collapsed / hidden by default, the data for the hidden row isn't sent.", @@ -129,126 +129,65 @@ "lastPlays": { "type": "array", "description": "An array containing JSON objects of the last played movies based on date.", - "example": [ - { - "$ref": "#/components/schemas/movie" - } - ] + "items": { + "$ref": "#/components/schemas/movie" + } }, "mostWatchedActors": { "type": "array", "description": "An array containing JSON objects of the most watched male actors.", - "example": [ - { - "id": 1, - "name": "Actor name", - "uniqueCount": 4, - "totalCount": 4, - "gender": "m", - "tmdb_poster_path": "/path_to_poster_on_tmdb.jpg", - "poster_path": "/storage/images/person/path_to_poster_on_local_storage.jpg" - } - ] + "items": { + "$ref": "#/components/schemas/male" + } }, "mostWatchedActresses": { "type": "array", "description": "An array containing JSON objects of the most watched actresses.", - "example": [ - { - "id": 1, - "name": "Actress name", - "uniqueCount": 2, - "totalCount": 2, - "gender": "f", - "tmdb_poster_path": "/path_to_poster_on_tmdb.jpg", - "poster_path": "/storage/images/person/path_to_poster_on_local_storage.jpg" - } - ] + "items": { + "$ref": "#/components/schemas/female" + } }, "mostWatchedDirectors": { "type": "array", "description": "An array containing JSON objects of the most watched directors.", - "example": [ - { - "id": 1, - "name": "Director name", - "uniqueCount": 2, - "totalCount": 2, - "gender": "m", - "tmdb_poster_path": "/path_to_poster_on_tmdb.jpg", - "poster_path": "/storage/images/person/path_to_poster_on_local_storage.jpg" - } - ] + "items": { + "$ref": "#/components/schemas/male" + } }, "mostWatchedLanguages": { "type": "array", "description": "An array containing JSON objects of the most watched languages.", - "example": [ - { - "language": "en", - "count": 7, - "name": "English", - "code": "en" - } - ] + "items": { + "$ref": "#/components/schemas/language" + } }, "mostWatchedGenres": { "type": "array", "description": "An array containing JSON objects of the most watched genres.", - "example": [ - { - "name": "Adventure", - "count": 7 - }, - { - "name": "Action", - "count": 6 - }, - { - "name": "Science Fiction", - "count": 3 - }, - { - "name": "Fantasy", - "count": 1 - } - ] + "items": { + "$ref": "#/components/schemas/genre" + } }, "mostWatchedProductionCompanies": { "type": "array", - "description": "An array containing JSON objects of the most watched production companies and the watched movies they produced.", - "example": [ - { - "name": "Lucasfilm Ltd.", - "count": 4, - "origin_country": "US", - "movies": [ - "Indiana Jones and the Temple of Doom", - "Raiders of the Lost Ark", - "Indiana Jones and the Kingdom of the Crystal Skull", - "Indiana Jones and the Last Crusade" - ] - } - ] + "description": "An array containing JSON objects of the most watched production companies, the watched movies they produced and their country of origin.", + "items": { + "$ref": "#/components/schemas/productioncompany" + } }, "mostWatchedReleaseYears": { "type": "array", "description": "An array containing JSON objects of the most watched release years.", - "example": [ - { - "name": 2023, - "count": 1 - } - ] + "items": { + "$ref": "#/components/schemas/releaseYears" + } }, "watchlistItems": { "type": "array", "description": "An array containing JSON objects of the items in the watchlist.", - "example": [ - { - "$ref": "#/components/schemas/movie" - } - ] + "items": { + "$ref": "#/components/schemas/movie" + } } } } @@ -264,6 +203,365 @@ } } }, + "/users/{username}\/statistics/lastplays": { + "get": { + "tags": [ + "Statistics" + ], + "summary": "Get last plays", + "description": "Gets the last plays for use in lazy-loading the data shown in the user's dashboard", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/movie" + } + } + } + } + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "description": "The requested user does not exist." + } + } + } + }, + "/users/{username}\/statistics/mostwatchedactors": { + "get": { + "tags": [ + "Statistics" + ], + "summary": "Get most watched actors", + "description": "Gets the most watched actors for use in lazy-loading the data shown in the user's dashboard", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/female" + } + } + } + } + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "description": "The requested user does not exist." + } + } + } + }, + "/users/{username}\/statistics/mostwatchedactresses": { + "get": { + "tags": [ + "Statistics" + ], + "summary": "Get most watched actresses", + "description": "Gets the most watched actresses for use in lazy-loading the data shown in the user's dashboard", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/male" + } + } + } + } + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "description": "The requested user does not exist." + } + } + } + }, + "/users/{username}\/statistics/mostwatcheddirectors": { + "get": { + "tags": [ + "Statistics" + ], + "summary": "Get most watched directors", + "description": "Gets the most watched directors for use in lazy-loading the data shown in the user's dashboard", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/male" + } + } + } + } + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "description": "The requested user does not exist." + } + } + } + }, + "/users/{username}/statistics/mostwatchedlanguages": { + "get": { + "tags": [ + "Statistics" + ], + "summary": "Get most watched languages", + "description": "Get list of the languages that have been watched the most, together with the amount of times they have been watched.", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/language" + } + } + } + } + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "description": "The requested user does not exist." + } + } + } + }, + "/users/{username}/statistics/mostwatchedgenres": { + "get": { + "tags": [ + "Statistics" + ], + "summary": "Get most watched genres", + "description": "Get list of the genres that have been watched the most, together with the amount of times they have been watched.", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/genre" + } + } + } + } + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "description": "The requested user does not exist." + } + } + } + }, + "/users/{username}/statistics/mostwatchedproductioncompanies": { + "get": { + "tags": [ + "Statistics" + ], + "summary": "Get most watched production companies", + "description": "Get list of the production companies that have been watched the most, together with the amount of times they have been watched, the watched movies they've produced and their country of origin.", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/productioncompany" + } + } + } + } + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "description": "The requested user does not exist." + } + } + } + }, + "/users/{username}/statistics/mostwatchedreleaseyears": { + "get": { + "tags": [ + "Statistics" + ], + "summary": "Get most watched release years", + "description": "Get list of the release years that have been watched the most, together with the amount of times they have been watched.", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/releaseYears" + } + } + } + } + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "description": "The requested user does not exist." + } + } + } + }, + "/users/{username}/statistics/watchlist": { + "get": { + "tags": [ + "Statistics" + ], + "summary": "Get movies in the watchlist", + "description": "Get all the movies in the watchlist of the user.", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "Name of user", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/movie" + } + } + } + } + }, + "403": { + "$ref": "#/components/responses/403" + }, + "404": { + "description": "The requested user does not exist." + } + } + } + }, "\/users\/{username}\/history\/movies": { "get": { "tags": [ @@ -1513,6 +1811,165 @@ }, "components": { "schemas": { + "male": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "The ID Movary has assigned this person", + "example": 32 + }, + "name": { + "type": "string", + "description": "The name of the person", + "example": "Peron name" + }, + "uniqueCount": { + "type": "integer", + "example": 1 + }, + "totalCount": { + "type": "integer", + "example": 1 + }, + "gender": { + "type": "string", + "example": "m", + "description": "Can be either f (for female) or m (for male)" + }, + "tmdb_poster_path": { + "type": "string", + "description": "The filepath of the poster from the TMDB API", + "example": "/tmdb_poster.jpg" + }, + "poster_path": { + "type": "string", + "description": "The filepath of the poster on the local Kovary installation from the root directory of Movary", + "example": "\/storage\/images\/person\/32.jpg" + } + } + }, + "female": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "The ID Movary has assigned this person", + "example": 32 + }, + "name": { + "type": "string", + "description": "The name of the person", + "example": "Peron name" + }, + "uniqueCount": { + "type": "integer", + "example": 1 + }, + "totalCount": { + "type": "integer", + "example": 1 + }, + "gender": { + "type": "string", + "example": "f", + "description": "Can be either f (for female) or m (for male)" + }, + "tmdb_poster_path": { + "type": "string", + "description": "The filepath of the poster from the TMDB API", + "example": "/tmdb_poster.jpg" + }, + "poster_path": { + "type": "string", + "description": "The filepath of the poster on the local Kovary installation from the root directory of Movary", + "example": "\/storage\/images\/person\/32.jpg" + } + } + }, + "productioncompany": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the production company", + "example": "Disney" + }, + "count": { + "type": "integer", + "description": "The amount of watched movies this production company has released", + "example": 2 + }, + "origin_country": { + "type": "string", + "description": "The country where this production company was founded", + "example": "US" + }, + "movies": { + "type": "array", + "description": "The list of watched movies this production company has released", + "example": [ + "Movie 1", + "Movie 2" + ] + } + } + }, + "language": { + "type": "object", + "properties": { + "language": { + "type": "string", + "description": "Two-letter code of the language", + "example": "en" + }, + "count": { + "type": "integer", + "description": "The amount of times watched movies have occured in this language", + "example": 8 + }, + "name": { + "type": "string", + "description": "The name of the language in speaking format", + "example": "English" + }, + "code": { + "type": "string", + "description": "Two-letter code of the language", + "example": "en" + } + } + }, + "genre": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the genre", + "example": "Action" + }, + "count": { + "type": "integer", + "description": "The amount of times watched movies contain this genre", + "example": 8 + } + } + }, + "releaseYears": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The year in four digits", + "example": "2024" + }, + "count": { + "type": "integer", + "description": "The amount of times watched movies have been released in this year", + "example": 8 + } + } + }, "movie": { "type": "object", "properties": { diff --git a/settings/routes.php b/settings/routes.php index 6d5fad4e9..87c8af73f 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -204,7 +204,8 @@ function addApiRoutes(RouterService $routerService, FastRoute\RouteCollector $ro $routes->add('DELETE', '/authentication/token', [Api\AuthenticationController::class, 'destroyToken']); $routes->add('GET', '/authentication/token', [Api\AuthenticationController::class, 'getTokenData']); - $routes->add('GET', '/users/{username:[a-zA-Z0-9]+}/dashboard', [Api\DashboardController::class, 'getDashboardData'], [Api\Middleware\IsAuthorizedToReadUserData::class]); + $routes->add('GET', '/users/{username:[a-zA-Z0-9]+}/statistics/dashboard', [Api\StatisticsController::class, 'getDashboardData'], [Api\Middleware\IsAuthorizedToReadUserData::class]); + $routes->add('GET', '/users/{username:[a-zA-Z0-9]+}/statistics/{statistic:[a-zA-Z]+}', [Api\StatisticsController::class, 'getStatistic'], [Api\Middleware\IsAuthorizedToReadUserData::class]); $routeUserHistory = '/users/{username:[a-zA-Z0-9]+}/history/movies'; $routes->add('GET', $routeUserHistory, [Api\HistoryController::class, 'getHistory'], [Api\Middleware\IsAuthorizedToReadUserData::class]); diff --git a/src/HttpController/Api/DashboardController.php b/src/HttpController/Api/StatisticsController.php similarity index 61% rename from src/HttpController/Api/DashboardController.php rename to src/HttpController/Api/StatisticsController.php index 3497e05f6..e08dba097 100644 --- a/src/HttpController/Api/DashboardController.php +++ b/src/HttpController/Api/StatisticsController.php @@ -14,15 +14,15 @@ use Movary\ValueObject\Http\Request; use Movary\ValueObject\Http\Response; -class DashboardController +readonly class StatisticsController { public function __construct( - private readonly MovieHistoryApi $movieHistoryApi, - private readonly MovieApi $movieApi, - private readonly MovieWatchlistApi $movieWatchlistApi, - private readonly UserPageAuthorizationChecker $userPageAuthorizationChecker, - private readonly DashboardFactory $dashboardFactory, - private readonly UserApi $userApi, + private MovieHistoryApi $movieHistoryApi, + private MovieApi $movieApi, + private MovieWatchlistApi $movieWatchlistApi, + private UserPageAuthorizationChecker $userPageAuthorizationChecker, + private DashboardFactory $dashboardFactory, + private UserApi $userApi, ) { } @@ -82,4 +82,49 @@ public function getDashboardData(Request $request) : Response } return Response::createJson(Json::encode($response)); } + + // phpcs:ignore + public function getStatistic(Request $request) : Response + { + $requestedUser = $this->userApi->findUserByName((string)$request->getRouteParameters()['username']); + if ($requestedUser === null) { + return Response::createNotFound(); + } + $userId = $requestedUser->getId(); + $requestedStatistic = strtolower($request->getRouteParameters()['statistic'] ?? ''); + $response = null; + switch($requestedStatistic) { + case 'lastplays': + $response = $this->movieHistoryApi->fetchLastPlays($userId); + break; + case 'mostwatchedactors': + $response = $this->movieHistoryApi->fetchActors($userId, 6, 1, gender: Gender::createMale(), personFilterUserId: $userId); + break; + case 'mostwatchedactresses': + $response = $this->movieHistoryApi->fetchActors($userId, 6, 1, gender: Gender::createFemale(), personFilterUserId: $userId); + break; + case 'mostwatcheddirectors': + $response = $this->movieHistoryApi->fetchDirectors($userId, 6, 1, personFilterUserId: $userId); + break; + case 'mostwatchedlanguages': + $response = $this->movieHistoryApi->fetchMostWatchedLanguages($userId); + break; + case 'mostwatchedgenres': + $response = $this->movieHistoryApi->fetchMostWatchedGenres($userId); + break; + case 'mostwatchedproductioncompanies': + $response = $this->movieHistoryApi->fetchMostWatchedProductionCompanies($userId, 12); + break; + case 'mostwatchedreleaseyears': + $response = $this->movieHistoryApi->fetchMostWatchedReleaseYears($userId); + break; + case 'watchlist': + $response = $this->movieWatchlistApi->fetchWatchlistPaginated($userId, 6, 1); + break; + } + if($response === null) { + return Response::createNotFound(); + } + return Response::createJson(Json::encode($response)); + } } diff --git a/tests/rest/api/user-dashboard.http b/tests/rest/api/user-dashboard.http deleted file mode 100644 index e15bd7011..000000000 --- a/tests/rest/api/user-dashboard.http +++ /dev/null @@ -1,5 +0,0 @@ -GET http://127.0.0.1/api/users/{{username}}/dashboard -Accept: */* -Cache-Control: no-cache -Content-Type: application/json -X-Movary-Client: REST Unit Test \ No newline at end of file diff --git a/tests/rest/api/user-statistics.http b/tests/rest/api/user-statistics.http new file mode 100644 index 000000000..97e68fab5 --- /dev/null +++ b/tests/rest/api/user-statistics.http @@ -0,0 +1,78 @@ +#@name Dashboard statistics +GET http://127.0.0.1/api/users/{{username}}/statistics/dashboard +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: REST Unit Test + +### +#@name Last plays +GET http://127.0.0.1/api/users/{{username}}/statistics/lastplays +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: REST Unit Test + +### +#@name Most watched actors +GET http://127.0.0.1/api/users/{{username}}/statistics/mostwatchedactors +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: REST Unit Test + +### +#@name Most watched actresses +GET http://127.0.0.1/api/users/{{username}}/statistics/mostwatchedactresses +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: REST Unit Test + +### +#@name Most watched directors +GET http://127.0.0.1/api/users/{{username}}/statistics/mostwatcheddirectors +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: REST Unit Test + +### +#@name Most watched languages +GET http://127.0.0.1/api/users/{{username}}/statistics/mostwatchedlanguages +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: REST Unit Test + +### +#@name Most watched genres +GET http://127.0.0.1/api/users/{{username}}/statistics/mostwatchedgenres +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: REST Unit Test + +### +#@name Most watched production companies +GET http://127.0.0.1/api/users/{{username}}/statistics/mostwatchedproductioncompanies +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: REST Unit Test + +### +#@name Most watched release years +GET http://127.0.0.1/api/users/{{username}}/statistics/mostwatchedreleaseyears +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: REST Unit Test + +### +#@name Watchlist +GET http://127.0.0.1/api/users/{{username}}/statistics/watchlist +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Movary-Client: REST Unit Test From 936b795eb58d871cd675f072e1cedc01d5c0e63b Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Fri, 8 Mar 2024 18:19:44 +0100 Subject: [PATCH 18/21] Don't send stats if the row is invisible Signed-off-by: JVT038 <47184046+JVT038@users.noreply.github.com> --- src/HttpController/Api/StatisticsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HttpController/Api/StatisticsController.php b/src/HttpController/Api/StatisticsController.php index e08dba097..32906152e 100644 --- a/src/HttpController/Api/StatisticsController.php +++ b/src/HttpController/Api/StatisticsController.php @@ -58,7 +58,7 @@ public function getDashboardData(Request $request) : Response ]; foreach($dashboardRows as $row) { - if($row->isExtended()) { + if($row->isExtended() && $row->isVisible()) { if($row->isLastPlays()) { $response['lastPlays'] = $this->movieHistoryApi->fetchLastPlays($userId); } elseif($row->isMostWatchedActors()) { From 697b4f3dfa3c5b8a9af3f2e6cf32ee825c370b6e Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:15:57 +0100 Subject: [PATCH 19/21] Fix middleware Request parameter --- src/HttpController/Api/Middleware/CreateUserMiddleware.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/HttpController/Api/Middleware/CreateUserMiddleware.php b/src/HttpController/Api/Middleware/CreateUserMiddleware.php index 15b302c75..301ba51b8 100644 --- a/src/HttpController/Api/Middleware/CreateUserMiddleware.php +++ b/src/HttpController/Api/Middleware/CreateUserMiddleware.php @@ -5,6 +5,7 @@ use Movary\Domain\User\UserApi; use Movary\HttpController\Web\Middleware\MiddlewareInterface; use Movary\ValueObject\Http\Response; +use Movary\ValueObject\Http\Request; class CreateUserMiddleware implements MiddlewareInterface { @@ -14,7 +15,7 @@ public function __construct( ) { } - public function __invoke() : ?Response + public function __invoke(Request $request) : ?Response { if ($this->registrationEnabled === false && $this->userApi->hasUsers() === true) { return Response::createForbidden(); From f382943123e39a3fd30aba70ff53029d71110906 Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:30:39 +0100 Subject: [PATCH 20/21] Fix comment and response body token --- src/HttpController/Api/CreateUserController.php | 2 +- tests/rest/api/create-user.http | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/HttpController/Api/CreateUserController.php b/src/HttpController/Api/CreateUserController.php index e5478f26e..63e20364b 100644 --- a/src/HttpController/Api/CreateUserController.php +++ b/src/HttpController/Api/CreateUserController.php @@ -67,7 +67,7 @@ public function createUser(Request $request) : Response return Response::createJson( Json::encode([ 'userId' => $userAndAuthToken['user']->getId(), - 'authToken' => $userAndAuthToken['token'] + 'token' => $userAndAuthToken['token'] ]), ); } catch (UsernameInvalidFormat) { diff --git a/tests/rest/api/create-user.http b/tests/rest/api/create-user.http index 009e5da90..a7fc03eec 100644 --- a/tests/rest/api/create-user.http +++ b/tests/rest/api/create-user.http @@ -20,7 +20,7 @@ X-Movary-Client: RestAPI Test %} ### -#@name Test missing input erro +#@name Test missing input error POST http://127.0.0.1/api/create-user Accept: */* @@ -176,7 +176,8 @@ DELETE http://127.0.0.1/api/authentication/token Accept: */* Cache-Control: no-cache Content-Type: application/json -X-Auth-Token: {{responseAuthToken}} +X-Movary-Client: RestAPI Test +X-Movary-Token: {{responseAuthToken}} > {% client.test("Response has correct status code", function() { From 7c3fc4b3ff34bf33e48c4891711322daad4b0c6c Mon Sep 17 00:00:00 2001 From: JVT038 <47184046+JVT038@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:50:58 +0100 Subject: [PATCH 21/21] Fixed middleware for createuser --- settings/routes.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/settings/routes.php b/settings/routes.php index 419d0bba1..a018f8a38 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -21,8 +21,7 @@ function addWebRoutes(RouterService $routerService, FastRoute\RouteCollector $ro $routes->add('GET', '/login', [Web\AuthenticationController::class, 'renderLoginPage'], [Web\Middleware\UserIsUnauthenticated::class]); $routes->add('GET', '/create-user', [Web\CreateUserController::class, 'renderPage'], [ Web\Middleware\UserIsUnauthenticated::class, - Web\Middleware\ServerHasUsers::class, - Web\Middleware\ServerHasRegistrationEnabled::class + Api\Middleware\CreateUserMiddleware::class ]); $routes->add('GET', '/docs/api', [Web\OpenApiController::class, 'renderPage']);
{{ subtitle }}