Skip to content

Commit

Permalink
Merge pull request #50 from leepeuker/add-change-password-ui
Browse files Browse the repository at this point in the history
Add password update to settings page
  • Loading branch information
leepeuker authored Jul 14, 2022
2 parents 5ba770c + 619b7f2 commit d97419e
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 21 deletions.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `[email protected]` 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)
Expand All @@ -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.

<a name="#install-via-docker"></a>

Expand Down
5 changes: 5 additions & 0 deletions settings/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
18 changes: 18 additions & 0 deletions src/Application/User/Api.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
}
Expand Down Expand Up @@ -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();
Expand All @@ -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);
}
Expand Down
16 changes: 16 additions & 0 deletions src/Application/User/Exception/PasswordTooShort.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types=1);

namespace Movary\Application\User\Exception;

class PasswordTooShort extends \Exception
{
public function __construct(private readonly int $minLength)
{
parent::__construct();
}

public function getMinLength() : int
{
return $this->minLength;
}
}
5 changes: 3 additions & 2 deletions src/Application/User/Service/Authentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
{
}

Expand Down Expand Up @@ -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();
}

Expand Down
4 changes: 4 additions & 0 deletions src/Command/ChangeUserPassword.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand Down
54 changes: 44 additions & 10 deletions src/HttpController/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? '-',
Expand All @@ -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(),
Expand Down
55 changes: 50 additions & 5 deletions templates/page/settings.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,53 @@
<main role="main" class="container">
{{ include('component/navbar.html.twig') }}

<div style="text-align: center;padding-top: 1rem">
<div style="padding-bottom: 1rem">
<div style="text-align: center;">

<div style="background-color: #F8F8F8;padding-top: 1rem;padding-bottom: 1rem">
<h5>Change password</h5>
<form action="/user/password" method="post" style="padding-top: 0.3rem">
<div class="input-group input-group-sm mb-3">
<input type="password" class="form-control" name="currentPassword" placeholder="Current password" required
style="margin-left: 10%;margin-right: 10%;text-align: center;">
</div>

<div class="input-group input-group-sm mb-3">
<input type="password" class="form-control" name="newPassword" placeholder="New password" required minlength="8"
style="margin-left: 10%;margin-right: 10%;text-align: center;">
</div>

<div class="input-group input-group-sm mb-3">
<input type="password" class="form-control" name="newPasswordRepeat" placeholder="Repeat password" required minlength="8"
style="margin-left: 10%;margin-right: 10%;text-align: center;">
</div>

{% if passwordErrorNotEqual == true %}
<div class="alert alert-danger alert-dismissible" role="alert" style="margin-left: 10%;margin-right: 10%;margin-bottom: 0.7rem!important;">
Passwords were not equal. <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% if passwordErrorMinLength == true %}
<div class="alert alert-danger alert-dismissible" role="alert" style="margin-left: 10%;margin-right: 10%;margin-bottom: 0.7rem!important;">
Password must be at least {{ passwordErrorMinLength }} characters long. <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% if passwordErrorCurrentInvalid == true %}
<div class="alert alert-danger alert-dismissible" role="alert" style="margin-left: 10%;margin-right: 10%;margin-bottom: 0.7rem!important;">
Current password is not correct. <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% if passwordUpdated == true %}
<div class="alert alert-success alert-dismissible" role="alert" style="margin-left: 10%;margin-right: 10%;margin-bottom: 0.7rem!important;">
Password was updated. <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<button class="btn btn-primary btn-sm" type="submit">Submit</button>
</form>
</div>

<hr style="margin: 0;padding: 0">

<div style="padding-top: 1rem;padding-bottom: 1rem">
<h5 style="padding-bottom: 0.5rem">trakt.tv</h5>

<p>To generate a client id visit this url <a href="https://trakt.tv/oauth/applications">here</a>.</p>
Expand All @@ -34,22 +79,22 @@
<input type="text" class="form-control" name="traktClientId" placeholder="Enter client ID here" value="{{ traktClientId }}"
style="margin-left: 10%;margin-right: 10%;text-align: center;">
</div>
<button class="btn btn-primary btn-sm" type="submit">Update</button>
<button class="btn btn-primary btn-sm" type="submit">Submit</button>
</form>

</div>

<hr style="margin: 0;padding: 0">

<div style="background-color: #F8F8F8;padding-top: 1rem;padding-bottom: 1rem">
<h5>Plex webhook url:</h5>
<h5>Plex webhook url</h5>

<p id="plexWebhookUrl" class="overflow-auto text-nowrap plexWebhookUrl" style="margin-left: 10%;margin-right: 10%">-</p>
<button id="deletePlexWebhookIdButton" type="button" class="btn btn-danger disabled btn-sm" onclick="deletePlexWebhookId()">Delete url</button>
<button type="button" class="btn btn-primary btn-sm" onclick="regeneratePlexWebhookId()">Regenerate url</button>
</div>

<hr style="margin: 0;padding: 0">

<div style="padding-top: 1rem;padding-bottom: 1rem">

<h5>Export & Import</h5>
Expand Down

0 comments on commit d97419e

Please sign in to comment.