diff --git a/README.md b/README.md
index 0eecd751..83c35428 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@ Web application to track and rate your watched movies.
Demo installation can be found [here](https://movary-demo.leepeuker.dev/) (login with user `movary@movary.com` and password `movary`)
+**Please report all bugs, improvement suggestions or feature wishes by creating [github issues](https://github.com/leepeuker/movary/issues)!**
+
1. [About](#install-via-docker)
2. [Install via docker](#install-via-docker)
3. [Important: First steps](#important-first-steps)
@@ -19,20 +21,24 @@ Demo installation can be found [here](https://movary-demo.leepeuker.dev/) (login
This is a web application to track and rate your watched movies (like a digital movie diary).
-It was created because I wanted a self hosted solution instead of using external providers like trakt.tv or letterboxd and I wanted the focus to be on MY watch history (-> no
+It was created because I wanted a self hosted solution instead of using external providers like trakt.tv or letterboxd and I wanted the focus to be on my personal watch history (->
+no
social media features).
+It has support for multiple users accounts if you want to share it with friends.
+
**Features:**
- add or update movie watch dates and ratings (only possible when logged in)
- statistics about your watched movies (e.g. most watched actors, most watched directors, most watched genres etc)
- PWA: can be installed as an app ([How to install PWAs in chrome](https://support.google.com/chrome/answer/9658361?hl=en&co=GENIE.Platform%3DAndroid&oco=1))
- import watched movies and ratings from trakt.tv and/or letterboxd.com
-- connect with plex to automatically log watched movies (plex premium required)
+- connect with plex via webhook to automatically log watched movies (plex premium required)
- uses themoviedb.org API for movie data
-- export your data as csv
+- export your personal data
-**Disclaimer:** This project is still in an experimental (but imo usable) state. I am planning to add more and improve existing features before creating a 1.0 realease.
+**Disclaimer:** This project is still in an experimental (but imo usable) state. I am planning to add more and improve existing features before creating a 1.0 realease, which can
+lead to breaking changes until then, so keep the release notes in mind when updating.
diff --git a/settings/routes.php b/settings/routes.php
index 6160ea69..dceaf651 100644
--- a/settings/routes.php
+++ b/settings/routes.php
@@ -112,6 +112,11 @@
'/user/trakt',
[\Movary\HttpController\SettingsController::class, 'updateTrakt']
);
+ $routeCollector->addRoute(
+ 'POST',
+ '/user/password',
+ [\Movary\HttpController\SettingsController::class, 'updatePassword']
+ );
$routeCollector->addRoute(
'DELETE',
'/user/plex-webhook-id',
diff --git a/src/Application/User/Api.php b/src/Application/User/Api.php
index ac68d7db..f2270ace 100644
--- a/src/Application/User/Api.php
+++ b/src/Application/User/Api.php
@@ -2,10 +2,13 @@
namespace Movary\Application\User;
+use Movary\Application\User\Exception\PasswordTooShort;
use Ramsey\Uuid\Uuid;
class Api
{
+ const PASSWORD_MIN_LENGTH = 8;
+
public function __construct(private readonly Repository $repository)
{
}
@@ -40,6 +43,17 @@ public function findUserIdByPlexWebhookId(string $webhookId) : ?int
return $this->repository->findUserIdByPlexWebhookId($webhookId);
}
+ public function isValidPassword(int $userId, string $password) : bool
+ {
+ $passwordHash = $this->repository->findUserById($userId)?->getPasswordHash();
+
+ if ($passwordHash === null) {
+ return false;
+ }
+
+ return password_verify($password, $passwordHash) === true;
+ }
+
public function regeneratePlexWebhookId(int $userId) : string
{
$plexWebhookId = Uuid::uuid4()->toString();
@@ -51,6 +65,10 @@ public function regeneratePlexWebhookId(int $userId) : string
public function updatePassword(int $userId, string $newPassword) : void
{
+ if (strlen($newPassword) < self::PASSWORD_MIN_LENGTH) {
+ throw new PasswordTooShort(self::PASSWORD_MIN_LENGTH);
+ }
+
if ($this->repository->findUserById($userId) === null) {
throw new \RuntimeException('There is no user with id: ' . $userId);
}
diff --git a/src/Application/User/Exception/PasswordTooShort.php b/src/Application/User/Exception/PasswordTooShort.php
new file mode 100644
index 00000000..9ed200c8
--- /dev/null
+++ b/src/Application/User/Exception/PasswordTooShort.php
@@ -0,0 +1,16 @@
+minLength;
+ }
+}
diff --git a/src/Application/User/Service/Authentication.php b/src/Application/User/Service/Authentication.php
index 5a566e1c..8cedb5ed 100644
--- a/src/Application/User/Service/Authentication.php
+++ b/src/Application/User/Service/Authentication.php
@@ -2,6 +2,7 @@
namespace Movary\Application\User\Service;
+use Movary\Application\User\Api;
use Movary\Application\User\Exception\EmailNotFound;
use Movary\Application\User\Exception\InvalidPassword;
use Movary\Application\User\Repository;
@@ -13,7 +14,7 @@ class Authentication
private const MAX_EXPIRATION_AGE_IN_DAYS = 30;
- public function __construct(private readonly Repository $repository)
+ public function __construct(private readonly Repository $repository, private readonly Api $userApi)
{
}
@@ -67,7 +68,7 @@ public function login(string $email, string $password, bool $rememberMe) : void
throw EmailNotFound::create();
}
- if (password_verify($password, $user->getPasswordHash()) === false) {
+ if ($this->userApi->isValidPassword($user->getId(), $password) === false) {
throw InvalidPassword::create();
}
diff --git a/src/Command/ChangeUserPassword.php b/src/Command/ChangeUserPassword.php
index 6f0b1eef..4dbed4d8 100644
--- a/src/Command/ChangeUserPassword.php
+++ b/src/Command/ChangeUserPassword.php
@@ -35,6 +35,10 @@ protected function execute(InputInterface $input, OutputInterface $output) : int
try {
$this->userApi->updatePassword($userId, $password);
+ } catch (User\Exception\PasswordTooShort $t) {
+ $this->generateOutput($output, "Error: Password must be at least {$t->getMinLength()} characters long.");
+
+ return Command::FAILURE;
} catch (\Throwable $t) {
$this->logger->error('Could not change password.', ['exception' => $t]);
diff --git a/src/HttpController/SettingsController.php b/src/HttpController/SettingsController.php
index d71beb62..e98dfd53 100644
--- a/src/HttpController/SettingsController.php
+++ b/src/HttpController/SettingsController.php
@@ -30,10 +30,20 @@ public function render() : Response
$userId = $this->authenticationService->getCurrentUserId();
+ $passwordErrorNotEqual = empty($_SESSION['passwordErrorNotEqual']) === false ? true : null;
+ $passwordErrorMinLength = empty($_SESSION['passwordErrorMinLength']) === false ? $_SESSION['passwordErrorMinLength'] : null;
+ $passwordErrorCurrentInvalid = empty($_SESSION['passwordErrorCurrentInvalid']) === false ? $_SESSION['passwordErrorCurrentInvalid'] : null;
+ $passwordUpdated = empty($_SESSION['passwordUpdated']) === false ? $_SESSION['passwordUpdated'] : null;
+ unset($_SESSION['passwordUpdated'], $_SESSION['passwordErrorCurrentInvalid'], $_SESSION['passwordErrorMinLength'], $_SESSION['passwordErrorNotEqual']);
+
return Response::create(
StatusCode::createOk(),
$this->twig->render('page/settings.html.twig', [
'plexWebhookUrl' => $this->userApi->findPlexWebhookId($userId) ?? '-',
+ 'passwordErrorNotEqual' => $passwordErrorNotEqual,
+ 'passwordErrorMinLength' => $passwordErrorMinLength,
+ 'passwordErrorCurrentInvalid' => $passwordErrorCurrentInvalid,
+ 'passwordUpdated' => $passwordUpdated,
'traktClientId' => $this->userApi->findTraktClientId($userId),
'traktUserName' => $this->userApi->findTraktUserName($userId),
'applicationVersion' => $this->applicationVersion ?? '-',
@@ -44,25 +54,49 @@ public function render() : Response
);
}
- public function updateTrakt(Request $request) : Response
+ public function updatePassword(Request $request) : Response
{
if ($this->authenticationService->isUserAuthenticated() === false) {
return Response::createFoundRedirect('/');
}
- $traktClientId = $request->getPostParameters()['traktClientId'];
- $traktUserName = $request->getPostParameters()['traktUserName'];
- $userId = $this->authenticationService->getCurrentUserId();
+ $newPassword = $request->getPostParameters()['newPassword'];
+ $newPasswordRepeat = $request->getPostParameters()['newPasswordRepeat'];
+ $currentPassword = $request->getPostParameters()['currentPassword'];
+
+ if ($this->userApi->isValidPassword($this->authenticationService->getCurrentUserId(), $currentPassword) === false) {
+ $_SESSION['passwordErrorCurrentInvalid'] = true;
- if (empty($traktClientId) === true) {
- $traktClientId = null;
+ return Response::create(
+ StatusCode::createSeeOther(),
+ null,
+ [Header::createLocation($_SERVER['HTTP_REFERER'])]
+ );
}
- if (empty($traktUserName) === true) {
- $traktUserName = null;
+
+ if ($newPassword !== $newPasswordRepeat) {
+ $_SESSION['passwordErrorNotEqual'] = true;
+
+ return Response::create(
+ StatusCode::createSeeOther(),
+ null,
+ [Header::createLocation($_SERVER['HTTP_REFERER'])]
+ );
}
- $this->userApi->updateTraktClientId($userId, $traktClientId);
- $this->userApi->updateTraktUserName($userId, $traktUserName);
+ if (strlen($newPassword) < 8) {
+ $_SESSION['passwordErrorMinLength'] = 8;
+
+ return Response::create(
+ StatusCode::createSeeOther(),
+ null,
+ [Header::createLocation($_SERVER['HTTP_REFERER'])]
+ );
+ }
+
+ $this->userApi->updatePassword($this->authenticationService->getCurrentUserId(), $newPassword);
+
+ $_SESSION['passwordUpdated'] = true;
return Response::create(
StatusCode::createSeeOther(),
diff --git a/templates/page/settings.html.twig b/templates/page/settings.html.twig
index 12ea15f4..3709295e 100644
--- a/templates/page/settings.html.twig
+++ b/templates/page/settings.html.twig
@@ -16,8 +16,53 @@
{{ include('component/navbar.html.twig') }}
-