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 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 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
2fac957
Merge remote-tracking branch 'origin/main' into add-create-user-endpoint
JVT038 Dec 1, 2024
697b4f3
Fix middleware Request parameter
JVT038 Dec 1, 2024
f382943
Fix comment and response body token
JVT038 Dec 1, 2024
7c3fc4b
Fixed middleware for createuser
JVT038 Dec 1, 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 @@ -435,7 +435,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
20 changes: 8 additions & 12 deletions settings/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,9 @@ 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,
Web\Middleware\ServerHasRegistrationEnabled::class
Api\Middleware\CreateUserMiddleware::class
]);
$routes->add('GET', '/docs/api', [Web\OpenApiController::class, 'renderPage']);

Expand Down Expand Up @@ -191,13 +185,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 @@ -210,6 +199,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 @@ -230,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']);
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
37 changes: 23 additions & 14 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,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;
Expand Down Expand Up @@ -64,6 +65,15 @@ class Factory

private const bool 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 All @@ -74,7 +84,7 @@ public static function createConfig(ContainerInterface $container) : Config

return new Config(
$container->get(File::class),
array_merge($fpmEnvironment, $systemEnvironment),
array_merge($fpmEnvironment, $systemEnvironment)
);
}

Expand All @@ -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),
);
}

Expand All @@ -106,23 +114,23 @@ public static function createDatabaseMigrationMigrateCommand(ContainerInterface
{
return new Command\DatabaseMigrationMigrate(
$container->get(PhinxApplication::class),
self::createDirectoryAppRoot() . 'settings/phinx.php',
self::createDirectoryAppRoot() . 'settings/phinx.php'
);
}

public static function createDatabaseMigrationRollbackCommand(ContainerInterface $container) : Command\DatabaseMigrationRollback
{
return new Command\DatabaseMigrationRollback(
$container->get(PhinxApplication::class),
self::createDirectoryAppRoot() . 'settings/phinx.php',
self::createDirectoryAppRoot() . 'settings/phinx.php'
);
}

public static function createDatabaseMigrationStatusCommand(ContainerInterface $container) : Command\DatabaseMigrationStatus
{
return new Command\DatabaseMigrationStatus(
$container->get(PhinxApplication::class),
self::createDirectoryAppRoot() . 'settings/phinx.php',
self::createDirectoryAppRoot() . 'settings/phinx.php'
);
}

Expand Down Expand Up @@ -191,15 +199,15 @@ public static function createJobController(ContainerInterface $container) : JobC
$container->get(JobQueueApi::class),
$container->get(LetterboxdCsvValidator::class),
$container->get(SessionWrapper::class),
self::createDirectoryStorageApp(),
self::createDirectoryStorageApp()
);
}

public static function createJobQueueScheduler(ContainerInterface $container, Config $config) : JobQueueScheduler
{
return new JobQueueScheduler(
$container->get(JobQueueApi::class),
self::getTmdbEnabledImageCaching($config),
self::getTmdbEnabledImageCaching($config)
);
}

Expand Down Expand Up @@ -240,10 +248,11 @@ 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(
$config->getAsBool('ENABLE_REGISTRATION', false),
return new HttpController\Api\Middleware\CreateUserMiddleware(
$container->get(UserApi::class),
$config->getAsBool('ENABLE_REGISTRATION', false)
);
}

Expand All @@ -260,7 +269,7 @@ public static function createTmdbApiClient(ContainerInterface $container) : Tmdb
{
return new Tmdb\TmdbClient(
$container->get(ClientInterface::class),
$container->get(ServerSettings::class),
$container->get(ServerSettings::class)
);
}

Expand Down Expand Up @@ -324,7 +333,7 @@ public static function createUrlGenerator(ContainerInterface $container, Config
return new UrlGenerator(
$container->get(TmdbUrlGenerator::class),
$container->get(ImageCacheService::class),
self::getTmdbEnabledImageCaching($config),
self::getTmdbEnabledImageCaching($config)
);
}

Expand Down Expand Up @@ -372,7 +381,7 @@ private static function createLoggerStreamHandlerFile(ContainerInterface $contai
{
$streamHandler = new StreamHandler(
self::createDirectoryStorageLogs() . 'app.log',
self::getLogLevel($config),
self::getLogLevel($config)
);
$streamHandler->setFormatter($container->get(LineFormatter::class));

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(),
'token' => $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()],
);
}
}
}
Loading
Loading