Skip to content

Commit

Permalink
Merge pull request #70 from leepeuker/username-instead-of-id-in-routes
Browse files Browse the repository at this point in the history
User username as route parameter instead of user id
  • Loading branch information
leepeuker authored Jul 25, 2022
2 parents ca0ed0a + e9b7372 commit b8f2da2
Show file tree
Hide file tree
Showing 34 changed files with 386 additions and 83 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ app_database_rollback:
make exec_app_cmd CMD="php bin/console.php database:migration:rollback"

app_user_create_test:
make exec_app_cmd CMD="php bin/console.php user:create a@a a"
make exec_app_cmd CMD="php bin/console.php user:create a@a aaaaaaaa a"

app_sync_trakt:
make exec_app_cmd CMD="php bin/console.php trakt:sync --overwrite --userId=1"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ volumes:
## Important: First steps
- Run database migrations: `docker exec movary php bin/console.php database:migration:migrate`
- Create a user: `docker exec movary php bin/console.php user:create example@email.com your-password`
- Create a user: `docker exec movary php bin/console.php user:create email@example.com password username`

List all available cli commands: `docker exec movary php bin/console.php`

Expand Down Expand Up @@ -188,7 +188,7 @@ Example cli sync (import history and ratings for user with id 1):
Use if you want to overwrite the local state with the trakt state (deletes and overwrites local data)
- `--ignore-cache`
Use if you want to sync everything from trakt regardless if there was a change since the last sync.
<a name="#letterboxd-import"></a>
### Letterboxd.com Import
Expand Down
25 changes: 25 additions & 0 deletions db/migrations/20220725113013_UpdateUsername.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php declare(strict_types=1);

use Phinx\Migration\AbstractMigration;

final class UpdateUsername extends AbstractMigration
{
public function down() : void
{
$this->execute(
<<<SQL
ALTER TABLE user MODIFY COLUMN name VARCHAR(256) DEFAULT NULL AFTER email;
SQL
);
}

public function up() : void
{
$this->execute(
<<<SQL
UPDATE user SET name = id WHERE name IS NULL;
ALTER TABLE user MODIFY COLUMN name VARCHAR(256) NOT NULL AFTER email;
SQL
);
}
}
17 changes: 11 additions & 6 deletions settings/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
);
$routeCollector->addRoute(
'GET',
'/{userId:\d+}/dashboard',
'/{username:.+}/dashboard',
[\Movary\HttpController\DashboardController::class, 'render']
);
$routeCollector->addRoute(
Expand Down Expand Up @@ -39,7 +39,7 @@
);
$routeCollector->addRoute(
'GET',
'/{userId:\d+}/history',
'/{username:.+}/history',
[\Movary\HttpController\HistoryController::class, 'renderHistory']
);
$routeCollector->addRoute(
Expand Down Expand Up @@ -69,12 +69,12 @@
);
$routeCollector->addRoute(
'GET',
'/{userId:\d+}/most-watched-actors',
'/{username:.+}/most-watched-actors',
[\Movary\HttpController\MostWatchedActorsController::class, 'renderPage']
);
$routeCollector->addRoute(
'GET',
'/{userId:\d+}/most-watched-directors',
'/{username:.+}/most-watched-directors',
[\Movary\HttpController\MostWatchedDirectorsController::class, 'renderPage']
);
$routeCollector->addRoute(
Expand All @@ -94,7 +94,7 @@
);
$routeCollector->addRoute(
'GET',
'/{userId:\d+}/movie/{id:\d+}',
'/{username:.+}/movie/{id:\d+}',
[\Movary\HttpController\MovieController::class, 'renderPage']
);
$routeCollector->addRoute(
Expand All @@ -109,7 +109,7 @@
);
$routeCollector->addRoute(
'GET',
'/{userId:\d+}/person/{id:\d+}',
'/{username:.+}/person/{id:\d+}',
[\Movary\HttpController\PersonController::class, 'renderPage']
);
$routeCollector->addRoute(
Expand All @@ -132,6 +132,11 @@
'/user/password',
[\Movary\HttpController\SettingsController::class, 'updatePassword']
);
$routeCollector->addRoute(
'POST',
'/user/name',
[\Movary\HttpController\SettingsController::class, 'updateName']
);
$routeCollector->addRoute(
'GET',
'/user/delete-ratings',
Expand Down
36 changes: 27 additions & 9 deletions src/Application/User/Api.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,24 @@

namespace Movary\Application\User;

use Movary\Application\User\Exception\PasswordTooShort;
use Movary\Application\User\Service\Validator;
use Ramsey\Uuid\Uuid;

class Api
{
private const PASSWORD_MIN_LENGTH = 8;

public function __construct(private readonly Repository $repository)
{
public function __construct(
private readonly Repository $repository,
private readonly Validator $userValidator
) {
}

public function createUser(string $email, string $password, ?string $name) : void
public function createUser(string $email, string $password, string $name) : void
{
$this->userValidator->ensureEmailIsUnique($email);
$this->userValidator->ensurePasswordIsValid($password);
$this->userValidator->ensureNameFormatIsValid($name);
$this->userValidator->ensureNameIsUnique($name);

$this->repository->createUser($email, password_hash($password, PASSWORD_DEFAULT), $name);
}

Expand Down Expand Up @@ -70,6 +75,11 @@ public function findTraktUserName(int $userId) : ?string
return $this->repository->findTraktUserName($userId);
}

public function findUserByName(string $name) : ?Entity
{
return $this->repository->findUserByName($name);
}

public function findUserIdByPlexWebhookId(string $webhookId) : ?int
{
return $this->repository->findUserIdByPlexWebhookId($webhookId);
Expand Down Expand Up @@ -107,14 +117,22 @@ public function updateDateFormatId(int $userId, int $dateFormat) : void

public function updateEmail(int $userId, string $email) : void
{
$this->userValidator->ensureEmailIsUnique($email, $userId);

$this->repository->updateEmail($userId, $email);
}

public function updateName(int $userId, string $name) : void
{
$this->userValidator->ensureNameFormatIsValid($name);
$this->userValidator->ensureNameIsUnique($name, $userId);

$this->repository->updateName($userId, $name);
}

public function updatePassword(int $userId, string $newPassword) : void
{
if (strlen($newPassword) < self::PASSWORD_MIN_LENGTH) {
throw new PasswordTooShort(self::PASSWORD_MIN_LENGTH);
}
$this->userValidator->ensurePasswordIsValid($newPassword);

if ($this->repository->findUserById($userId) === null) {
throw new \RuntimeException('There is no user with id: ' . $userId);
Expand Down
13 changes: 10 additions & 3 deletions src/Application/User/Entity.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ class Entity
{
private function __construct(
private readonly int $id,
private readonly string $name,
private readonly string $passwordHash,
private readonly bool $areCoreAccountChangesDisabled,
private readonly int $dateFormatId,
private readonly ?string $plexWebhookUuid,
private readonly ?int $dateFormatId,
private readonly ?string $TraktUserName,
private readonly ?string $TraktClientId,
) {
Expand All @@ -19,10 +20,11 @@ public static function createFromArray(array $data) : self
{
return new self(
(int)$data['id'],
$data['name'],
$data['password'],
(bool)$data['core_account_changes_disabled'],
$data['plex_webhook_uuid'],
$data['date_format_id'],
$data['plex_webhook_uuid'],
$data['trakt_user_name'],
$data['trakt_client_id'],
);
Expand All @@ -33,7 +35,7 @@ public function areCoreAccountChangesDisabled() : bool
return $this->areCoreAccountChangesDisabled;
}

public function getDateFormatId() : ?int
public function getDateFormatId() : int
{
return $this->dateFormatId;
}
Expand All @@ -43,6 +45,11 @@ public function getId() : int
return $this->id;
}

public function getName() : string
{
return $this->name;
}

public function getPasswordHash() : string
{
return $this->passwordHash;
Expand Down
11 changes: 11 additions & 0 deletions src/Application/User/Exception/EmailNotUnique.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace Movary\Application\User\Exception;

class EmailNotUnique extends InvalidCredentials
{
public static function create() : self
{
return new self('Email is not unique.');
}
}
11 changes: 11 additions & 0 deletions src/Application/User/Exception/UsernameInvalidFormat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace Movary\Application\User\Exception;

class UsernameInvalidFormat extends InvalidCredentials
{
public static function create() : self
{
return new self('Name is not valid.');
}
}
11 changes: 11 additions & 0 deletions src/Application/User/Exception/UsernameNotUnique.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace Movary\Application\User\Exception;

class UsernameNotUnique extends InvalidCredentials
{
public static function create() : self
{
return new self('Name is not valid.');
}
}
26 changes: 25 additions & 1 deletion src/Application/User/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function createAuthToken(int $userId, string $token, DateTime $expiration
);
}

public function createUser(string $email, string $passwordHash, ?string $name) : void
public function createUser(string $email, string $passwordHash, string $name) : void
{
$this->dbConnection->insert(
'user',
Expand Down Expand Up @@ -132,6 +132,17 @@ public function findUserById(int $userId) : ?Entity
return Entity::createFromArray($data);
}

public function findUserByName(string $name) : ?Entity
{
$data = $this->dbConnection->fetchAssociative('SELECT * FROM `user` WHERE `name` = ?', [$name]);

if (empty($data) === true) {
return null;
}

return Entity::createFromArray($data);
}

public function findUserIdByAuthToken(string $token) : ?int
{
$id = $this->dbConnection->fetchOne('SELECT `user_id` FROM `user_auth_token` WHERE `token` = ?', [$token]);
Expand Down Expand Up @@ -206,6 +217,19 @@ public function updateEmail(int $userId, string $email) : void
);
}

public function updateName(int $userId, string $name) : void
{
$this->dbConnection->update(
'user',
[
'name' => $name,
],
[
'id' => $userId,
]
);
}

public function updatePassword(int $userId, string $passwordHash) : void
{
$this->dbConnection->update(
Expand Down
6 changes: 6 additions & 0 deletions src/Application/User/Service/Authentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Movary\Application\User\Service;

use Movary\Application\User\Api;
use Movary\Application\User\Entity;
use Movary\Application\User\Exception\EmailNotFound;
use Movary\Application\User\Exception\InvalidPassword;
use Movary\Application\User\Repository;
Expand All @@ -23,6 +24,11 @@ public function deleteToken(string $token) : void
$this->repository->deleteAuthToken($token);
}

public function getCurrentUser() : Entity
{
return $this->userApi->fetchUser($this->getCurrentUserId());
}

public function getCurrentUserId() : int
{
$userId = $_SESSION['userId'] ?? null;
Expand Down
57 changes: 57 additions & 0 deletions src/Application/User/Service/Validator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php declare(strict_types=1);

namespace Movary\Application\User\Service;

use Movary\Application\User\Exception\EmailNotUnique;
use Movary\Application\User\Exception\PasswordTooShort;
use Movary\Application\User\Exception\UsernameInvalidFormat;
use Movary\Application\User\Exception\UsernameNotUnique;
use Movary\Application\User\Repository;

class Validator
{
private const PASSWORD_MIN_LENGTH = 8;

public function __construct(private readonly Repository $repository)
{
}

public function ensureEmailIsUnique(string $email, ?int $expectUserId = null) : void
{
$user = $this->repository->findUserByEmail($email);
if ($user === null) {
return;
}

if ($user->getId() !== $expectUserId) {
throw new EmailNotUnique();
}
}

public function ensureNameFormatIsValid(string $name) : void
{
preg_match('~^[a-zA-Z0-9]+$~', $name, $matches);
if (empty($matches) === true) {
throw new UsernameInvalidFormat();
}
}

public function ensureNameIsUnique(string $name, ?int $expectUserId = null) : void
{
$user = $this->repository->findUserByName($name);
if ($user === null) {
return;
}

if ($user->getId() !== $expectUserId) {
throw new UsernameNotUnique();
}
}

public function ensurePasswordIsValid(string $password) : void
{
if (strlen($password) < self::PASSWORD_MIN_LENGTH) {
throw new PasswordTooShort(self::PASSWORD_MIN_LENGTH);
}
}
}
Loading

0 comments on commit b8f2da2

Please sign in to comment.