Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API endpoint to create new user #590

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f9441e0
Added an API endpoint to fetch data for the dashboard
JVT038 Feb 19, 2024
efb12be
Added OpenAPI spec for the dashboard API endpoint
JVT038 Feb 19, 2024
3b4d033
Added 403 and 404 to OpenAPI and added HTTP test
JVT038 Feb 19, 2024
4e7ec34
Moved `MovieRatingController` to API routes and added OpenAPI specs a…
JVT038 Feb 22, 2024
70afcdc
Removed old endpoints
JVT038 Feb 23, 2024
29ab92c
Fix tests
JVT038 Feb 23, 2024
73aa4e0
Fix tests
JVT038 Feb 23, 2024
767bf8c
Fix tests
JVT038 Feb 23, 2024
2820e35
Merge branch 'main' into add-statistics-endpoint
JVT038 Feb 24, 2024
8f7f1d0
Merge branch 'main' into add-user-rating-api-endpoint
JVT038 Feb 24, 2024
720692c
Fix OpenAPI spec
JVT038 Feb 24, 2024
b02461d
Add API endpoint to create a new user
JVT038 Feb 26, 2024
37ee3f9
Fix tests
JVT038 Feb 26, 2024
0f187d7
Add HTTP tests
JVT038 Feb 26, 2024
58070d6
Merge branch 'main' into add-user-rating-api-endpoint
JVT038 Feb 27, 2024
dc8843b
Merge branch 'main' into add-statistics-endpoint
JVT038 Feb 27, 2024
cf629d4
Merge branch 'main' into add-create-user-endpoint
JVT038 Feb 27, 2024
5c0f196
Fix tests
JVT038 Feb 27, 2024
f589c70
Fix tests
JVT038 Feb 27, 2024
46ba46b
Add newline to fix test
JVT038 Feb 27, 2024
9aa2e6b
Fix phpstan test
JVT038 Feb 27, 2024
fd4551f
Merge branch 'main' into add-create-user-endpoint
JVT038 Feb 28, 2024
42b45d4
Merge branch 'main' into add-statistics-endpoint
JVT038 Mar 6, 2024
823e547
Added API endpoints to retrieve individual statistics.
JVT038 Mar 8, 2024
936b795
Don't send stats if the row is invisible
JVT038 Mar 8, 2024
63f3fbf
Merge pull request #1 from JVT038/add-statistics-endpoint
JVT038 May 27, 2024
438a6df
Merge pull request #2 from JVT038/add-user-rating-api-endpoint
JVT038 May 27, 2024
bc9ac35
Merge branch 'main' into add-create-user-endpoint
JVT038 May 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']),
Expand All @@ -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, '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']),
Expand Down
918 changes: 911 additions & 7 deletions docs/openapi.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
Expand Down
27 changes: 27 additions & 0 deletions public/js/createnewuser.js
Original file line number Diff line number Diff line change
@@ -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');
});
}
10 changes: 6 additions & 4 deletions public/js/movie.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
17 changes: 7 additions & 10 deletions settings/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ function addWebRoutes(RouterService $routerService, FastRoute\RouteCollector $ro

$routes->add('GET', '/', [Web\LandingPageController::class, 'render'], [Web\Middleware\UserIsUnauthenticated::class, Web\Middleware\ServerHasNoUsers::class]);
$routes->add('GET', '/login', [Web\AuthenticationController::class, 'renderLoginPage'], [Web\Middleware\UserIsUnauthenticated::class]);
$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,
Expand Down Expand Up @@ -184,13 +179,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, true);
}
Expand All @@ -203,6 +193,10 @@ function addApiRoutes(RouterService $routerService, FastRoute\RouteCollector $ro
$routes->add('POST', '/authentication/token', [Api\AuthenticationController::class, 'createToken']);
$routes->add('DELETE', '/authentication/token', [Api\AuthenticationController::class, 'destroyToken']);
$routes->add('GET', '/authentication/token', [Api\AuthenticationController::class, 'getTokenData']);
$routes->add('POST', '/create-user', [Api\CreateUserController::class, 'createUser'], [Api\Middleware\IsUnauthenticated::class, Api\Middleware\CreateUserMiddleware::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]);
Expand All @@ -223,6 +217,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']);
Expand Down
2 changes: 1 addition & 1 deletion src/Domain/User/Service/Authentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/Domain/User/Service/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
18 changes: 13 additions & 5 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
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;
use Movary\Domain\User;
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;
Expand Down Expand Up @@ -65,6 +65,15 @@ class Factory

private const DEFAULT_ENABLE_FILE_LOGGING = true;

public static function createAuthenticationController(ContainerInterface $container) : AuthenticationController
{
return new AuthenticationController(
$container->get(Twig\Environment::class),
$container->get(Authentication::class),
$container->get(SessionWrapper::class)
);
}

public static function createConfig(ContainerInterface $container) : Config
{
$dotenv = Dotenv::createMutable(self::createDirectoryAppRoot());
Expand Down Expand Up @@ -92,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),
);
}

Expand Down Expand Up @@ -241,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 createCreateUserMiddleware(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)
);
}
Expand Down
115 changes: 115 additions & 0 deletions src/HttpController/Api/CreateUserController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

namespace Movary\HttpController\Api;

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\Json;
use Movary\ValueObject\Http\Header;
use Movary\ValueObject\Http\Request;
use Movary\ValueObject\Http\Response;
use Exception;

class CreateUserController
{
public const STRING MOVARY_WEB_CLIENT = 'Movary Web';
public function __construct(
private readonly Authentication $authenticationService,
private readonly UserApi $userApi,
) {
}

// phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
public function createUser(Request $request) : Response
{
$hasUsers = $this->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 (Exception) {
return Response::createBadRequest(
Json::encode([
'error' => 'GenericError',
'message' => 'Something has gone wrong. Please check the logs and try again later.'
]),
[Header::createContentTypeJson()],
);
}
}
}
25 changes: 25 additions & 0 deletions src/HttpController/Api/Middleware/CreateUserMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php declare(strict_types=1);

namespace Movary\HttpController\Api\Middleware;

use Movary\Domain\User\UserApi;
use Movary\HttpController\Web\Middleware\MiddlewareInterface;
use Movary\ValueObject\Http\Response;

class CreateUserMiddleware implements MiddlewareInterface
{
public function __construct(
readonly private UserApi $userApi,
readonly private bool $registrationEnabled
) {
}

public function __invoke() : ?Response
{
if ($this->registrationEnabled === false && $this->userApi->hasUsers() === true) {
return Response::createForbidden();
}

return null;
}
}
24 changes: 24 additions & 0 deletions src/HttpController/Api/Middleware/IsUnauthenticated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php declare(strict_types=1);

namespace Movary\HttpController\Api\Middleware;

use Movary\Domain\User\Service\Authentication;
use Movary\ValueObject\Http\Request;
use Movary\ValueObject\Http\Response;

class IsUnauthenticated implements MiddlewareInterface
{
public function __construct(
private readonly Authentication $authenticationService,
) {
}

public function __invoke(Request $request) : ?Response
{
if ($this->authenticationService->getUserIdByApiToken($request) !== null) {
return Response::createForbidden();
}

return null;
}
}
Loading
Loading