Parcourir la source

Added remember me
Fixed middleware execution order
closes #81

Sergio Brighenti il y a 5 ans
Parent
commit
748bd98abf

+ 2 - 1
CHANGELOG.md

@@ -1,12 +1,13 @@
 ## v.3.0 (WIP)
 + Upgraded from Slim3 to Slim 4.
 + 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.
 + Improved installer.
 + Improved thumbnail generation.
 + Replaced videojs player with Plyr.
 + Implemented SameSite XSS protection.
-+ Added ability to add custom HTML in <head> tag.
 + Small fixes and improvements.
 
 ## v.2.6.6

+ 32 - 1
app/Controllers/LoginController.php

@@ -28,6 +28,7 @@ class LoginController extends Controller
      * @param  Request  $request
      * @param  Response  $response
      * @return Response
+     * @throws \Exception
      */
     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->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')) {
             return redirect($response, $this->session->get('redirectTo'));
         }
@@ -66,14 +91,20 @@ class LoginController extends Controller
     }
 
     /**
+     * @param  Request  $request
      * @param  Response  $response
      * @return Response
      */
-    public function logout(Response $response): Response
+    public function logout(Request $request,Response $response): Response
     {
         $this->session->clear();
         $this->session->set('logged', false);
         $this->session->alert(lang('goodbye'), 'warning');
+
+        if (!empty($request->getCookieParams()['remember'])) {
+            setcookie('remember', null);
+        }
+
         return redirect($response, route('login.show'));
     }
 

+ 1 - 0
app/Controllers/UploadController.php

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

+ 2 - 39
app/Middleware/Middleware.php

@@ -2,50 +2,13 @@
 
 namespace App\Middleware;
 
-
-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 App\Controllers\Controller;
 use Psr\Http\Message\ResponseInterface as Response;
 use Psr\Http\Message\ServerRequestInterface as Request;
 use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
 
-/**
- * @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
+abstract class Middleware extends Controller
 {
-    /** @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

+ 40 - 0
app/Middleware/RememberMiddleware.php

@@ -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);
+    }
+}

+ 1 - 1
app/helpers.php

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

+ 4 - 2
bootstrap/app.php

@@ -5,6 +5,7 @@ use App\Exception\Handlers\AppErrorHandler;
 use App\Exception\Handlers\Renderers\HtmlErrorRenderer;
 use App\Factories\ViewFactory;
 use App\Middleware\InjectMiddleware;
+use App\Middleware\RememberMiddleware;
 use App\Web\Lang;
 use App\Web\Session;
 use Aws\S3\S3Client;
@@ -149,7 +150,8 @@ if (!$config['debug']) {
     $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
 $app->add(function (Request $request, RequestHandler $handler) {
@@ -173,7 +175,7 @@ $app->add(function (Request $request, RequestHandler $handler) {
     return $handler->handle($request);
 });
 
-$app->add(InjectMiddleware::class);
+$app->addRoutingMiddleware();
 
 // Configure the error handler
 $errorHandler = new AppErrorHandler($app->getCallableResolver(), $app->getResponseFactory());

+ 1 - 0
resources/lang/en.lang.php

@@ -107,4 +107,5 @@ return [
     '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_set' => 'Custom Head HTML applied successfully.',
+    'remember_me' => 'Remember me',
 ];

+ 7 - 0
resources/schemas/mysql/mysql.4.sql

@@ -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`);
+

+ 6 - 0
resources/schemas/sqlite/sqlite.4.sql

@@ -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`);

+ 7 - 3
resources/templates/auth/login.twig

@@ -26,8 +26,8 @@
 
 {% block content %}
     <div class="container-fluid">
-        <form class="form-signin text-center" method="post" action="{{ route('login') }}">
-            <div class="row">
+        <form class="form-signin" method="post" action="{{ route('login') }}">
+            <div class="row text-center">
                 <div class="col-md-12">
                     <h1 class="h3 mb-3 font-weight-normal">{{ config.app_name }}</h1>
                     {% include 'comp/alert.twig' %}
@@ -39,9 +39,13 @@
                     <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>
                     <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 class="row">
+            <div class="row mt-2">
                 <div class="col-md-12">
                     <button class="btn btn-lg btn-primary btn-block" type="submit">{{ lang('login') }}</button>
                 </div>

+ 2 - 2
resources/templates/dashboard/pager_header.twig

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

+ 9 - 1
resources/templates/upload/public.twig

@@ -67,6 +67,7 @@
                         <audio id="player" autoplay controls loop preload="auto">
                             <source src="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/raw') }}" type="{{ media.mimetype }}">
                             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>
                     </div>
                 {% elseif type is same as ('video') %}
@@ -74,8 +75,14 @@
                         <video id="player" autoplay controls loop preload="auto">
                             <source src="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ extension ~ '/raw') }}" type="{{ media.mimetype }}">
                             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>
                     </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 %}
                     <div class="text-center">
                         <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>
                 </div>
                 <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>
                 </div>
             </div>

+ 7 - 2
src/css/app.css

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

+ 1 - 1
src/js/app.js

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