Added remember me

Fixed middleware execution order
closes #81
This commit is contained in:
Sergio Brighenti 2019-11-15 15:47:51 +01:00
parent 5adb29d700
commit 748bd98abf
15 changed files with 122 additions and 53 deletions

View file

@ -1,12 +1,13 @@
## v.3.0 (WIP) ## v.3.0 (WIP)
+ Upgraded from Slim3 to Slim 4. + Upgraded from Slim3 to Slim 4.
+ Added web upload. + Added web upload.
+ Added ability to add custom HTML in \<head\> tag.
+ Added ability to show a preview of PDF files.
+ Raw URL now accept file extensions. + Raw URL now accept file extensions.
+ Improved installer. + Improved installer.
+ Improved thumbnail generation. + Improved thumbnail generation.
+ Replaced videojs player with Plyr. + Replaced videojs player with Plyr.
+ Implemented SameSite XSS protection. + Implemented SameSite XSS protection.
+ Added ability to add custom HTML in <head> tag.
+ Small fixes and improvements. + Small fixes and improvements.
## v.2.6.6 ## v.2.6.6

View file

@ -28,6 +28,7 @@ class LoginController extends Controller
* @param Request $request * @param Request $request
* @param Response $response * @param Response $response
* @return Response * @return Response
* @throws \Exception
*/ */
public function login(Request $request, Response $response): Response public function login(Request $request, Response $response): Response
{ {
@ -58,6 +59,30 @@ class LoginController extends Controller
$this->session->alert(lang('welcome', [$result->username]), 'info'); $this->session->alert(lang('welcome', [$result->username]), 'info');
$this->logger->info("User $result->username logged in."); $this->logger->info("User $result->username logged in.");
if (param($request, 'remember') === 'on') {
$selector = bin2hex(random_bytes(8));
$token = bin2hex(random_bytes(32));
$expire = time() + 604800; // a week
$this->database->query('UPDATE `users` SET `remember_selector`=?, `remember_token`=?, `remember_expire`=? WHERE `id`=?', [
$selector,
password_hash($token, PASSWORD_DEFAULT),
date('Y-m-d\TH:i:s', $expire),
$result->id,
]);
// Workaround for php <= 7.3
if (PHP_VERSION_ID < 70300) {
setcookie('remember', "{$selector}:{$token}", $expire, '; SameSite=Lax', '', false, true);
} else {
setcookie('remember', "{$selector}:{$token}", [
'expires' => $expire,
'httponly' => true,
'samesite' => 'Lax',
]);
}
}
if ($this->session->has('redirectTo')) { if ($this->session->has('redirectTo')) {
return redirect($response, $this->session->get('redirectTo')); return redirect($response, $this->session->get('redirectTo'));
} }
@ -66,14 +91,20 @@ class LoginController extends Controller
} }
/** /**
* @param Request $request
* @param Response $response * @param Response $response
* @return Response * @return Response
*/ */
public function logout(Response $response): Response public function logout(Request $request,Response $response): Response
{ {
$this->session->clear(); $this->session->clear();
$this->session->set('logged', false); $this->session->set('logged', false);
$this->session->alert(lang('goodbye'), 'warning'); $this->session->alert(lang('goodbye'), 'warning');
if (!empty($request->getCookieParams()['remember'])) {
setcookie('remember', null);
}
return redirect($response, route('login.show')); return redirect($response, route('login.show'));
} }

View file

@ -250,6 +250,7 @@ class UploadController extends Controller
* @param string|null $ext * @param string|null $ext
* @return Response * @return Response
* @throws FileNotFoundException * @throws FileNotFoundException
* @throws HttpBadRequestException
* @throws HttpNotFoundException * @throws HttpNotFoundException
*/ */
public function showRaw(Request $request, Response $response, string $userCode, string $mediaCode, ?string $ext = null): Response public function showRaw(Request $request, Response $response, string $userCode, string $mediaCode, ?string $ext = null): Response

View file

@ -2,50 +2,13 @@
namespace App\Middleware; namespace App\Middleware;
use App\Controllers\Controller;
use App\Database\DB;
use App\Web\Lang;
use App\Web\Session;
use App\Web\View;
use DI\Container;
use League\Flysystem\Filesystem;
use Monolog\Logger;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler; use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
/** abstract class Middleware extends Controller
* @property Session|null session
* @property View view
* @property DB|null database
* @property Logger|null logger
* @property Filesystem|null storage
* @property Lang lang
* @property array config
*/
abstract class Middleware
{ {
/** @var Container */
protected $container;
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* @param $name
* @return mixed|null
* @throws \DI\DependencyException
* @throws \DI\NotFoundException
*/
public function __get($name)
{
if ($this->container->has($name)) {
return $this->container->get($name);
}
return null;
}
/** /**
* @param Request $request * @param Request $request

View file

@ -0,0 +1,40 @@
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class RememberMiddleware extends Middleware
{
/**
* @param Request $request
* @param RequestHandler $handler
* @return Response
*/
public function __invoke(Request $request, RequestHandler $handler)
{
if (!$this->session->get('logged', false) && !empty($request->getCookieParams()['remember'])) {
list($selector, $token) = explode(':', $request->getCookieParams()['remember']);
$result = $this->database->query('SELECT `id`, `email`, `username`,`is_admin`, `active`, `remember_token` FROM `users` WHERE `remember_selector` = ? AND `remember_expire` > ? LIMIT 1',
[$selector, date('Y-m-d\TH:i:s', time())]
)->fetch();
if ($result && password_verify($token, $result->remember_token) && $result->active) {
$this->session->set('logged', true);
$this->session->set('user_id', $result->id);
$this->session->set('username', $result->username);
$this->session->set('admin', $result->is_admin);
$this->session->set('used_space', humanFileSize($this->getUsedSpaceByUser($result->id)));
}
}
return $handler->handle($request);
}
}

View file

@ -28,7 +28,7 @@ if (!function_exists('humanRandomString')) {
* @param int $length * @param int $length
* @return string * @return string
*/ */
function humanRandomString(int $length = 13): string function humanRandomString(int $length = 10): string
{ {
$result = ''; $result = '';
$numberOffset = round($length * 0.2); $numberOffset = round($length * 0.2);

View file

@ -5,6 +5,7 @@ use App\Exception\Handlers\AppErrorHandler;
use App\Exception\Handlers\Renderers\HtmlErrorRenderer; use App\Exception\Handlers\Renderers\HtmlErrorRenderer;
use App\Factories\ViewFactory; use App\Factories\ViewFactory;
use App\Middleware\InjectMiddleware; use App\Middleware\InjectMiddleware;
use App\Middleware\RememberMiddleware;
use App\Web\Lang; use App\Web\Lang;
use App\Web\Session; use App\Web\Session;
use Aws\S3\S3Client; use Aws\S3\S3Client;
@ -149,7 +150,8 @@ if (!$config['debug']) {
$app->getRouteCollector()->setCacheFile(BASE_DIR.'resources/cache/routes.cache.php'); $app->getRouteCollector()->setCacheFile(BASE_DIR.'resources/cache/routes.cache.php');
} }
$app->addRoutingMiddleware(); $app->add(InjectMiddleware::class);
$app->add(RememberMiddleware::class);
// Permanently redirect paths with a trailing slash to their non-trailing counterpart // Permanently redirect paths with a trailing slash to their non-trailing counterpart
$app->add(function (Request $request, RequestHandler $handler) { $app->add(function (Request $request, RequestHandler $handler) {
@ -173,7 +175,7 @@ $app->add(function (Request $request, RequestHandler $handler) {
return $handler->handle($request); return $handler->handle($request);
}); });
$app->add(InjectMiddleware::class); $app->addRoutingMiddleware();
// Configure the error handler // Configure the error handler
$errorHandler = new AppErrorHandler($app->getCallableResolver(), $app->getResponseFactory()); $errorHandler = new AppErrorHandler($app->getCallableResolver(), $app->getResponseFactory());

View file

@ -107,4 +107,5 @@ return [
'custom_head_html' => 'Custom HTML Head content', 'custom_head_html' => 'Custom HTML Head content',
'custom_head_html_hint' => 'This content will be added at the <head> tag on every page.', 'custom_head_html_hint' => 'This content will be added at the <head> tag on every page.',
'custom_head_set' => 'Custom Head HTML applied successfully.', 'custom_head_set' => 'Custom Head HTML applied successfully.',
'remember_me' => 'Remember me',
]; ];

View file

@ -0,0 +1,7 @@
ALTER TABLE `users`
ADD COLUMN `remember_selector` VARCHAR(16),
ADD COLUMN `remember_token` VARCHAR(256),
ADD COLUMN `remember_expire` TIMESTAMP;
ALTER TABLE `users` ADD INDEX (`remember_selector`);

View file

@ -0,0 +1,6 @@
ALTER TABLE `users` ADD COLUMN `remember_selector` VARCHAR(16);
ALTER TABLE `users` ADD COLUMN `remember_token` VARCHAR(256);
ALTER TABLE `users` ADD COLUMN `remember_expire` TIMESTAMP;
CREATE INDEX IF NOT EXISTS `remember_selector_index`
ON `users` (`remember_selector`);

View file

@ -26,8 +26,8 @@
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">
<form class="form-signin text-center" method="post" action="{{ route('login') }}"> <form class="form-signin" method="post" action="{{ route('login') }}">
<div class="row"> <div class="row text-center">
<div class="col-md-12"> <div class="col-md-12">
<h1 class="h3 mb-3 font-weight-normal">{{ config.app_name }}</h1> <h1 class="h3 mb-3 font-weight-normal">{{ config.app_name }}</h1>
{% include 'comp/alert.twig' %} {% include 'comp/alert.twig' %}
@ -39,9 +39,13 @@
<input type="text" id="username" class="form-control" placeholder="{{ lang('login.username') }}" name="username" required autofocus> <input type="text" id="username" class="form-control" placeholder="{{ lang('login.username') }}" name="username" required autofocus>
<label for="password" class="sr-only">{{ lang('password') }}</label> <label for="password" class="sr-only">{{ lang('password') }}</label>
<input type="password" id="password" class="form-control" placeholder="{{ lang('password') }}" name="password" required> <input type="password" id="password" class="form-control" placeholder="{{ lang('password') }}" name="password" required>
<div class="form-check">
<input type="checkbox" name="remember" class="form-check-input float-left" id="remember">
<label class="form-check-label" for="remember">{{ lang('remember_me') }}</label>
</div>
</div> </div>
</div> </div>
<div class="row"> <div class="row mt-2">
<div class="col-md-12"> <div class="col-md-12">
<button class="btn btn-lg btn-primary btn-block" type="submit">{{ lang('login') }}</button> <button class="btn btn-lg btn-primary btn-block" type="submit">{{ lang('login') }}</button>
</div> </div>

View file

@ -24,8 +24,8 @@
<a class="dropdown-item" href="{{ queryParams({'sort':'size'}) }}"><i class="fas fa-weight-hanging fa-fw"></i> {{ lang('size') }}</a> <a class="dropdown-item" href="{{ queryParams({'sort':'size'}) }}"><i class="fas fa-weight-hanging fa-fw"></i> {{ lang('size') }}</a>
</div> </div>
</div> </div>
<a href="{{ queryParams({'order': request.param('order') is same as('ASC') ? 'DESC' : 'ASC' }) }}" class="btn btn-outline-info"> <a href="{{ queryParams({'order': request.queryParams['order'] is same as('ASC') ? 'DESC' : 'ASC' }) }}" class="btn btn-outline-info">
<i class="fas {{ request.param('order') is same as('ASC') ? 'fa-sort-amount-up' : 'fa-sort-amount-down' }}"></i> <i class="fas {{ request.queryParams['order'] is same as('ASC') ? 'fa-sort-amount-up' : 'fa-sort-amount-down' }}"></i>
</a> </a>
</div> </div>
</div> </div>

View file

@ -67,6 +67,7 @@
<audio id="player" autoplay controls loop preload="auto"> <audio id="player" autoplay controls loop preload="auto">
<source src="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/raw') }}" type="{{ media.mimetype }}"> <source src="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/raw') }}" type="{{ media.mimetype }}">
Your browser does not support HTML5 audio. Your browser does not support HTML5 audio.
<a href="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/download') }}" class="btn btn-dark btn-lg"><i class="fas fa-cloud-download-alt fa-fw"></i> Download</a>
</audio> </audio>
</div> </div>
{% elseif type is same as ('video') %} {% elseif type is same as ('video') %}
@ -74,8 +75,14 @@
<video id="player" autoplay controls loop preload="auto"> <video id="player" autoplay controls loop preload="auto">
<source src="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/raw') }}" type="{{ media.mimetype }}"> <source src="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/raw') }}" type="{{ media.mimetype }}">
Your browser does not support HTML5 video. Your browser does not support HTML5 video.
<a href="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/download') }}" class="btn btn-dark btn-lg"><i class="fas fa-cloud-download-alt fa-fw"></i> Download</a>
</video> </video>
</div> </div>
{% elseif media.mimetype is same as ('application/pdf') %}
<object type="{{ media.mimetype }}" data="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/raw') }}" class="pdf-viewer">
Your browser does not support PDF previews.
<a href="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/download') }}" class="btn btn-dark btn-lg"><i class="fas fa-cloud-download-alt fa-fw"></i> Download</a>
</object>
{% else %} {% else %}
<div class="text-center"> <div class="text-center">
<div class="row mb-3"> <div class="row mb-3">
@ -116,7 +123,8 @@
<textarea type="text" class="form-control mb-2" id="telegram-share-text" onclick="this.select()">{{ media.filename }}</textarea> <textarea type="text" class="form-control mb-2" id="telegram-share-text" onclick="this.select()">{{ media.filename }}</textarea>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-info btn-block" id="telegram-share-button" onclick="app.telegramShare()" data-url="https://telegram.me/share/url?url={{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension) }}&text="><i class="fab fa-telegram-plane fa-lg fa-fw"></i> {{ lang("send") }}</button> <button type="button" class="btn btn-info btn-block" id="telegram-share-button" onclick="app.telegramShare()" data-url="https://telegram.me/share/url?url={{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension) }}&text=">
<i class="fab fa-telegram-plane fa-lg fa-fw"></i> {{ lang("send") }}</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ lang('cancel') }}</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">{{ lang('cancel') }}</button>
</div> </div>
</div> </div>

View file

@ -35,7 +35,7 @@ body {
} }
.form-signin input[type="password"] { .form-signin input[type="password"] {
margin-bottom: 10px; margin-bottom: .50rem;
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
} }
@ -95,7 +95,6 @@ body {
width: 77%; width: 77%;
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
} }
.media-audio { .media-audio {
@ -106,6 +105,7 @@ body {
.dropzone { .dropzone {
min-height: 300px; min-height: 300px;
border: 2px dashed rgba(0, 0, 0, 0.3); border: 2px dashed rgba(0, 0, 0, 0.3);
background: none;
padding: 20px 20px; padding: 20px 20px;
} }
@ -115,4 +115,9 @@ body {
.system-tile { .system-tile {
font-size: 2.5rem; font-size: 2.5rem;
}
.pdf-viewer {
width: 100%;
height: 85vh
} }

View file

@ -25,7 +25,7 @@ var app = {
}); });
new ClipboardJS('.btn-clipboard'); new ClipboardJS('.btn-clipboard');
new Plyr($('#player')); new Plyr($('#player'), {ratio: '16:9'});
$('.footer').fadeIn(600); $('.footer').fadeIn(600);