Procházet zdrojové kódy

Working on user registration, user disk quota, password recovery

Sergio Brighenti před 5 roky
rodič
revize
49c9e48e5e

+ 5 - 3
app/Controllers/AdminController.php

@@ -34,9 +34,10 @@ class AdminController extends Controller
             $totalSize += $filesystem->getSize($media->storage_path);
         }
 
-        $registerEnabled = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'register_enabled\'')->fetch()->value;
-        $hideByDefault = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'hide_by_default\'')->fetch()->value;
-        $copyUrl = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'copy_url_behavior\'')->fetch()->value;
+        $registerEnabled = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'register_enabled\'')->fetch()->value ?? 'off';
+        $hideByDefault = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'hide_by_default\'')->fetch()->value ?? 'off';
+        $copyUrl = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'copy_url_behavior\'')->fetch()->value ?? 'off';
+        $defaultUserQuota = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'default_user_quota\'')->fetch()->value ?? '1G';
 
         return view()->render($response, 'dashboard/system.twig', [
             'usersCount' => $usersCount,
@@ -52,6 +53,7 @@ class AdminController extends Controller
             'register_enabled' => $registerEnabled,
             'hide_by_default' => $hideByDefault,
             'copy_url_behavior' => $copyUrl,
+            'default_user_quota' => $defaultUserQuota,
         ]);
     }
 

+ 15 - 10
app/Controllers/LoginController.php → app/Controllers/Auth/LoginController.php

@@ -1,20 +1,21 @@
 <?php
 
-namespace App\Controllers;
+namespace App\Controllers\Auth;
 
+use App\Controllers\Controller;
 use Psr\Http\Message\ResponseInterface as Response;
 use Psr\Http\Message\ServerRequestInterface as Request;
 
 class LoginController extends Controller
 {
     /**
-     * @param Response $response
+     * @param  Response  $response
      *
-     * @throws \Twig\Error\LoaderError
+     * @return Response
      * @throws \Twig\Error\RuntimeError
      * @throws \Twig\Error\SyntaxError
      *
-     * @return Response
+     * @throws \Twig\Error\LoaderError
      */
     public function show(Response $response): Response
     {
@@ -22,16 +23,20 @@ class LoginController extends Controller
             return redirect($response, route('home'));
         }
 
-        return view()->render($response, 'auth/login.twig');
+        $registerEnabled = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'register_enabled\'')->fetch()->value ?? 'off';
+
+        return view()->render($response, 'auth/login.twig', [
+            'register_enabled' => $registerEnabled,
+        ]);
     }
 
     /**
-     * @param Request  $request
-     * @param Response $response
+     * @param  Request  $request
+     * @param  Response  $response
      *
+     * @return Response
      * @throws \Exception
      *
-     * @return Response
      */
     public function login(Request $request, Response $response): Response
     {
@@ -74,8 +79,8 @@ class LoginController extends Controller
     }
 
     /**
-     * @param Request  $request
-     * @param Response $response
+     * @param  Request  $request
+     * @param  Response  $response
      *
      * @return Response
      */

+ 12 - 0
app/Controllers/Auth/PasswordRecoveryController.php

@@ -0,0 +1,12 @@
+<?php
+
+
+namespace App\Controllers\Auth;
+
+
+use App\Controllers\Controller;
+
+class PasswordRecoveryController extends Controller
+{
+
+}

+ 120 - 0
app/Controllers/Auth/RegisterController.php

@@ -0,0 +1,120 @@
+<?php
+
+
+namespace App\Controllers\Auth;
+
+
+use App\Controllers\Controller;
+use Psr\Http\Message\ResponseInterface as Response;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Slim\Exception\HttpNotFoundException;
+
+class RegisterController extends Controller
+{
+
+    /**
+     * @param  Request  $request
+     * @param  Response  $response
+     * @return Response
+     * @throws HttpNotFoundException
+     * @throws \Twig\Error\LoaderError
+     * @throws \Twig\Error\RuntimeError
+     * @throws \Twig\Error\SyntaxError
+     */
+    public function registerForm(Request $request, Response $response): Response
+    {
+        if ($this->session->get('logged', false)) {
+            return redirect($response, route('home'));
+        }
+
+        $registerEnabled = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'register_enabled\'')->fetch()->value ?? 'off';
+        if ($registerEnabled === 'off') {
+            throw new HttpNotFoundException($request);
+        }
+
+        return view()->render($response, 'auth/register.twig');
+    }
+
+    /**
+     * @param  Request  $request
+     * @param  Response  $response
+     * @return Response
+     * @throws HttpNotFoundException
+     * @throws \Exception
+     */
+    public function register(Request $request, Response $response): Response
+    {
+        if ($this->session->get('logged', false)) {
+            return redirect($response, route('home'));
+        }
+
+        $registerEnabled = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'register_enabled\'')->fetch()->value ?? 'off';
+        if ($registerEnabled === 'off') {
+            throw new HttpNotFoundException($request);
+        }
+
+        if (param($request, 'email') === null && !filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL)) {
+            $this->session->alert(lang('email_required'), 'danger');
+
+            return redirect($response, route('register.show'));
+        }
+
+        if ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `email` = ?', param($request, 'email'))->fetch()->count > 0) {
+            $this->session->alert(lang('email_taken'), 'danger');
+
+            return redirect($response, route('register.show'));
+        }
+
+        if (param($request, 'username') === null) {
+            $this->session->alert(lang('username_required'), 'danger');
+
+            return redirect($response, route('register.show'));
+        }
+
+        if (param($request, 'password') === null) {
+            $this->session->alert(lang('password_required'), 'danger');
+
+            return redirect($response, route('register.show'));
+        }
+
+        if ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `username` = ?', param($request, 'username'))->fetch()->count > 0) {
+            $this->session->alert(lang('username_taken'), 'danger');
+
+            return redirect($response, route('register.show'));
+        }
+
+        do {
+            $userCode = humanRandomString(5);
+        } while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `user_code` = ?', $userCode)->fetch()->count > 0);
+
+        $token = $this->generateUserUploadToken();
+        $activateToken = bin2hex(random_bytes(16));
+
+        $this->database->query('INSERT INTO `users`(`email`, `username`, `password`, `is_admin`, `active`, `user_code`, `token`, `activate_token`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [
+            param($request, 'email'),
+            param($request, 'username'),
+            password_hash(param($request, 'password'), PASSWORD_DEFAULT),
+            0,
+            0,
+            $userCode,
+            $token,
+            $activateToken,
+        ]);
+
+        $this->session->alert(lang('register_success', [param($request, 'username')]), 'success');
+        $this->logger->info('New user registered.', [array_diff_key($request->getParsedBody(), array_flip(['password']))]);
+
+        return redirect($response, route('login.show'));
+    }
+
+    /**
+     * @param  Request  $request
+     * @param  Response  $response
+     * @param  string  $activateToken
+     * @return Response
+     */
+    public function activateUser(Request $request, Response $response, string $activateToken): Response
+    {
+
+    }
+}

+ 13 - 0
app/Controllers/Controller.php

@@ -126,4 +126,17 @@ abstract class Controller
             ]);
         }
     }
+
+
+    /**
+     * @return string
+     */
+    protected function generateUserUploadToken(): string
+    {
+        do {
+            $token = 'token_'.md5(uniqid('', true));
+        } while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `token` = ?', $token)->fetch()->count > 0);
+
+        return $token;
+    }
 }

+ 1 - 1
app/Controllers/DashboardController.php

@@ -58,7 +58,7 @@ class DashboardController extends Controller
             ->search(param($request, 'search', null))
             ->run($page);
 
-        $copyUrl = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'copy_url_behavior\'')->fetch()->value;
+        $copyUrl = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'copy_url_behavior\'')->fetch()->value ?? 'off';
 
         return view()->render(
             $response,

+ 1 - 1
app/Controllers/MediaController.php

@@ -66,7 +66,7 @@ class MediaController extends Controller
             throw new HttpNotFoundException($request);
         }
 
-        $copyUrl = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'copy_url_behavior\'')->fetch()->value;
+        $copyUrl = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'copy_url_behavior\'')->fetch()->value ?? 'off';
 
         return view()->render($response, 'upload/public.twig', [
             'delete_token' => $token,

+ 6 - 0
app/Controllers/SettingController.php

@@ -16,8 +16,14 @@ class SettingController extends Controller
      */
     public function saveSettings(Request $request, Response $response): Response
     {
+        if (!preg_match('/[0-9]+[K|M|G|T]/i', param($request, 'default_user_quota', '1G'))) {
+            $this->session->alert(lang('invalid_quota', 'danger'));
+            return redirect($response, route('system'));
+        }
+
         $this->updateSetting('register_enabled', param($request, 'register_enabled', 'off'));
         $this->updateSetting('hide_by_default', param($request, 'hide_by_default', 'off'));
+        $this->updateSetting('default_user_quota', param($request, 'default_user_quota', '1G'));
         $this->updateSetting('copy_url_behavior', param($request, 'copy_url_behavior') === null ? 'default' : 'raw');
 
         $this->applyTheme($request);

+ 1 - 1
app/Controllers/UploadController.php

@@ -101,7 +101,7 @@ class UploadController extends Controller
         } while ($this->database->query('SELECT COUNT(*) AS `count` FROM `uploads` WHERE `code` = ?', $code)->fetch()->count > 0);
 
         $published = 1;
-        if ($this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'hide_by_default\'')->fetch()->value === 'on') {
+        if (($this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'hide_by_default\'')->fetch()->value ?? 'off') === 'on') {
             $published = 0;
         }
 

+ 4 - 16
app/Controllers/UserController.php

@@ -62,7 +62,7 @@ class UserController extends Controller
      */
     public function store(Request $request, Response $response): Response
     {
-        if (param($request, 'email') === null) {
+        if (param($request, 'email') === null && !filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL)) {
             $this->session->alert(lang('email_required'), 'danger');
 
             return redirect($response, route('user.create'));
@@ -96,7 +96,7 @@ class UserController extends Controller
             $userCode = humanRandomString(5);
         } while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `user_code` = ?', $userCode)->fetch()->count > 0);
 
-        $token = $this->generateNewToken();
+        $token = $this->generateUserUploadToken();
 
         $this->database->query('INSERT INTO `users`(`email`, `username`, `password`, `is_admin`, `active`, `user_code`, `token`) VALUES (?, ?, ?, ?, ?, ?, ?)', [
             param($request, 'email'),
@@ -151,7 +151,7 @@ class UserController extends Controller
     {
         $user = $this->getUser($request, $id, false);
 
-        if (param($request, 'email') === null) {
+        if (param($request, 'email') === null && !filter_var(param($request, 'email'), FILTER_VALIDATE_EMAIL)) {
             $this->session->alert(lang('email_required'), 'danger');
 
             return redirect($response, route('user.edit', ['id' => $id]));
@@ -251,7 +251,7 @@ class UserController extends Controller
     {
         $user = $this->getUser($request, $id, true);
 
-        $token = $this->generateNewToken();
+        $token = $this->generateUserUploadToken();
 
         $this->database->query('UPDATE `users` SET `token`=? WHERE `id` = ?', [
             $token,
@@ -264,16 +264,4 @@ class UserController extends Controller
 
         return $response;
     }
-
-    /**
-     * @return string
-     */
-    protected function generateNewToken(): string
-    {
-        do {
-            $token = 'token_'.md5(uniqid('', true));
-        } while ($this->database->query('SELECT COUNT(*) AS `count` FROM `users` WHERE `token` = ?', $token)->fetch()->count > 0);
-
-        return $token;
-    }
 }

+ 3 - 1
app/helpers.php

@@ -22,7 +22,7 @@ if (!function_exists('humanFileSize')) {
         for ($i = 0; ($size / 1024) > 0.9; $i++, $size /= 1024) {
         }
 
-        return round($size, $precision).' '.['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'][$i];
+        return round($size, $precision).' '.['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'][$i];
     }
 }
 
@@ -88,6 +88,8 @@ if (!function_exists('stringToBytes')) {
 
         $val = (float) $val;
         switch ($last) {
+            case 't':
+                $val *= 1024;
             case 'g':
                 $val *= 1024;
             case 'm':

+ 4 - 2
app/routes.php

@@ -2,14 +2,14 @@
 
 // Auth routes
 use App\Controllers\AdminController;
+use App\Controllers\Auth\RegisterController;
 use App\Controllers\ClientController;
 use App\Controllers\DashboardController;
 use App\Controllers\ExportController;
-use App\Controllers\LoginController;
+use App\Controllers\Auth\LoginController;
 use App\Controllers\MediaController;
 use App\Controllers\ProfileController;
 use App\Controllers\SettingController;
-use App\Controllers\ThemeController;
 use App\Controllers\UpgradeController;
 use App\Controllers\UploadController;
 use App\Controllers\UserController;
@@ -63,6 +63,8 @@ $app->group('', function (RouteCollectorProxy $group) {
 })->add(App\Middleware\CheckForMaintenanceMiddleware::class)->add(AuthMiddleware::class);
 
 $app->get('/', [DashboardController::class, 'redirects'])->setName('root');
+$app->get('/register', [RegisterController::class, 'registerForm'])->setName('register.show');
+$app->post('/register', [RegisterController::class, 'register'])->setName('register');
 $app->get('/login', [LoginController::class, 'show'])->setName('login.show');
 $app->post('/login', [LoginController::class, 'login'])->setName('login');
 $app->map(['GET', 'POST'], '/logout', [LoginController::class, 'logout'])->setName('logout');

+ 119 - 114
resources/lang/en.lang.php

@@ -1,119 +1,124 @@
 <?php
 
 return [
-    'lang'                    => 'English',
-    'enforce_language'        => 'Enforce language',
-    'yes'                     => 'Yes',
-    'no'                      => 'No',
-    'send'                    => 'Send',
-    'no_media'                => 'No media found.',
-    'login.username'          => 'Username or E-Mail',
-    'password'                => 'Password',
-    'login'                   => 'Login',
-    'username'                => 'Username',
-    'home'                    => 'Home',
-    'users'                   => 'Users',
-    'system'                  => 'System',
-    'profile'                 => 'Profile',
-    'logout'                  => 'Logout',
-    'pager.next'              => 'Next',
-    'pager.previous'          => 'Previous',
-    'copy_link'               => 'Copy link',
-    'public.telegram'         => 'Share on Telegram',
-    'public.delete_text'      => 'Are you sure you want to delete this item? It will be gone forever!',
-    'preview'                 => 'Preview',
-    'filename'                => 'Filename',
-    'size'                    => 'Size',
-    'public'                  => 'Public',
-    'owner'                   => 'Owner',
-    'date'                    => 'Date',
-    'raw'                     => 'Show raw',
-    'download'                => 'Download',
-    'upload'                  => 'Upload',
-    'delete'                  => 'Delete',
-    'publish'                 => 'Publish',
-    'hide'                    => 'Hide',
-    'files'                   => 'Files',
-    'orphaned_files'          => 'Orphaned Files',
-    'theme'                   => 'Theme',
-    'click_to_load'           => 'Click to load...',
-    'apply'                   => 'Apply',
-    'save'                    => 'Save',
-    'used'                    => 'Used',
-    'php_info'                => 'PHP Informations',
-    'system_settings'         => 'System Settings',
-    'user.create'             => 'Create User',
-    'user.edit'               => 'Edit User',
-    'is_active'               => 'Is active',
-    'is_admin'                => 'Is administrator',
-    'your_profile'            => 'Your Profile',
-    'token'                   => 'Token',
-    'copy'                    => 'Copy',
-    'update'                  => 'Update',
-    'edit'                    => 'Edit',
-    'client_config'           => 'Client Configuration',
-    'user_code'               => 'User Code',
-    'active'                  => 'Active',
-    'admin'                   => 'Admin',
-    'reg_date'                => 'Registration Date',
-    'none'                    => 'None',
-    'open'                    => 'Open',
-    'confirm'                 => 'Confirmation',
-    'confirm_string'          => 'Are you sure?',
-    'installed'               => 'Installation completed successfully!',
-    'bad_login'               => 'Wrong credentials.',
-    'account_disabled'        => 'Your account is disabled.',
-    'welcome'                 => 'Welcome, %s!',
-    'goodbye'                 => 'Goodbye!',
-    'token_not_found'         => 'Token specified not found.',
-    'email_required'          => 'The email is required.',
-    'email_taken'             => 'The email is already taken.',
-    'username_required'       => 'The username is required.',
-    'username_taken'          => 'The username is already taken.',
-    'password_required'       => 'The password is required.',
-    'user_created'            => 'User "%s" created!',
-    'user_updated'            => 'User "%s" updated!',
-    'profile_updated'         => 'Profile updated successfully!',
-    'user_deleted'            => 'User deleted.',
-    'cannot_delete'           => 'You cannot delete yourself.',
-    'cannot_demote'           => 'You cannot demote yourself.',
-    'cannot_write_file'       => 'The destination path is not writable.',
-    'deleted_orphans'         => 'Successfully deleted %d orphaned files.',
-    'switch_to'               => 'Switch to',
-    'gallery'                 => 'Gallery',
-    'table'                   => 'Table',
-    'dotted_search'           => 'Search...',
-    'order_by'                => 'Order by...',
-    'time'                    => 'Time',
-    'name'                    => 'Name',
-    'maintenance'             => 'Maintenance',
-    'clean_orphaned_uploads'  => 'Clean Orphaned Uploads',
-    'path_not_writable'       => 'The output path is not writable.',
-    'already_latest_version'  => 'You already have the latest version.',
-    'new_version_available'   => 'New version %s available!',
-    'cannot_retrieve_file'    => 'Cannot retrieve the file.',
-    'file_size_no_match'      => 'The downloaded file doesn\'t match the correct file size.',
-    'check_for_updates'       => 'Check for updates',
-    'upgrade'                 => 'Upgrade',
-    'updates'                 => 'Updates',
+    'lang' => 'English',
+    'enforce_language' => 'Enforce language',
+    'yes' => 'Yes',
+    'no' => 'No',
+    'send' => 'Send',
+    'no_media' => 'No media found.',
+    'login.username' => 'Username or E-Mail',
+    'password' => 'Password',
+    'login' => 'Login',
+    'username' => 'Username',
+    'home' => 'Home',
+    'users' => 'Users',
+    'system' => 'System',
+    'profile' => 'Profile',
+    'logout' => 'Logout',
+    'pager.next' => 'Next',
+    'pager.previous' => 'Previous',
+    'copy_link' => 'Copy link',
+    'public.telegram' => 'Share on Telegram',
+    'public.delete_text' => 'Are you sure you want to delete this item? It will be gone forever!',
+    'preview' => 'Preview',
+    'filename' => 'Filename',
+    'size' => 'Size',
+    'public' => 'Public',
+    'owner' => 'Owner',
+    'date' => 'Date',
+    'raw' => 'Show raw',
+    'download' => 'Download',
+    'upload' => 'Upload',
+    'delete' => 'Delete',
+    'publish' => 'Publish',
+    'hide' => 'Hide',
+    'files' => 'Files',
+    'orphaned_files' => 'Orphaned Files',
+    'theme' => 'Theme',
+    'click_to_load' => 'Click to load...',
+    'apply' => 'Apply',
+    'save' => 'Save',
+    'used' => 'Used',
+    'php_info' => 'PHP Informations',
+    'system_settings' => 'System Settings',
+    'user.create' => 'Create User',
+    'user.edit' => 'Edit User',
+    'is_active' => 'Is active',
+    'is_admin' => 'Is administrator',
+    'your_profile' => 'Your Profile',
+    'token' => 'Token',
+    'copy' => 'Copy',
+    'update' => 'Update',
+    'edit' => 'Edit',
+    'client_config' => 'Client Configuration',
+    'user_code' => 'User Code',
+    'active' => 'Active',
+    'admin' => 'Admin',
+    'reg_date' => 'Registration Date',
+    'none' => 'None',
+    'open' => 'Open',
+    'confirm' => 'Confirmation',
+    'confirm_string' => 'Are you sure?',
+    'installed' => 'Installation completed successfully!',
+    'bad_login' => 'Wrong credentials.',
+    'account_disabled' => 'Your account is disabled.',
+    'welcome' => 'Welcome, %s!',
+    'goodbye' => 'Goodbye!',
+    'token_not_found' => 'Token specified not found.',
+    'email_required' => 'The email is required.',
+    'email_taken' => 'The email is already taken.',
+    'username_required' => 'The username is required.',
+    'username_taken' => 'The username is already taken.',
+    'password_required' => 'The password is required.',
+    'user_created' => 'User "%s" created!',
+    'user_updated' => 'User "%s" updated!',
+    'profile_updated' => 'Profile updated successfully!',
+    'user_deleted' => 'User deleted.',
+    'cannot_delete' => 'You cannot delete yourself.',
+    'cannot_demote' => 'You cannot demote yourself.',
+    'cannot_write_file' => 'The destination path is not writable.',
+    'deleted_orphans' => 'Successfully deleted %d orphaned files.',
+    'switch_to' => 'Switch to',
+    'gallery' => 'Gallery',
+    'table' => 'Table',
+    'dotted_search' => 'Search...',
+    'order_by' => 'Order by...',
+    'time' => 'Time',
+    'name' => 'Name',
+    'maintenance' => 'Maintenance',
+    'clean_orphaned_uploads' => 'Clean Orphaned Uploads',
+    'path_not_writable' => 'The output path is not writable.',
+    'already_latest_version' => 'You already have the latest version.',
+    'new_version_available' => 'New version %s available!',
+    'cannot_retrieve_file' => 'Cannot retrieve the file.',
+    'file_size_no_match' => 'The downloaded file doesn\'t match the correct file size.',
+    'check_for_updates' => 'Check for updates',
+    'upgrade' => 'Upgrade',
+    'updates' => 'Updates',
     'maintenance_in_progress' => 'Platform under maintenance, try again later...',
-    'cancel'                  => 'Cancel',
-    'auto_set'                => 'Set automatically',
-    'default_lang_behavior'   => 'XBackBone will try to match the browser language by default (the fallback is English).',
-    'prerelease_channel'      => 'Prerelease Channel',
-    'no_upload_token'         => 'You don\'t have a personal upload token. (Generate one and try again.)',
-    'drop_to_upload'          => 'Click or drop your files here to upload.',
-    'donation'                => 'Donation',
-    'donate_text'             => 'If you like XBackBone, consider a donation to support development!',
-    '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 HTML head applied.',
-    'remember_me'             => 'Remember me',
-    'please_wait'             => 'Please wait…',
-    'dont_close'              => 'Do not close this tab until completion.',
-    'register_enabled'              => 'Registration Enabled',
-    'hide_by_default'              => 'Hide uploads by default',
-    'copy_url_behavior'              => 'Copy URL mode',
-    'settings_saved'              => 'System settings saved!',
-    'export_data' => 'Export Data'
+    'cancel' => 'Cancel',
+    'auto_set' => 'Set automatically',
+    'default_lang_behavior' => 'XBackBone will try to match the browser language by default (the fallback is English).',
+    'prerelease_channel' => 'Prerelease Channel',
+    'no_upload_token' => 'You don\'t have a personal upload token. (Generate one and try again.)',
+    'drop_to_upload' => 'Click or drop your files here to upload.',
+    'donation' => 'Donation',
+    'donate_text' => 'If you like XBackBone, consider a donation to support development!',
+    '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 HTML head applied.',
+    'remember_me' => 'Remember me',
+    'please_wait' => 'Please wait…',
+    'dont_close' => 'Do not close this tab until completion.',
+    'register_enabled' => 'Registrations enabled',
+    'hide_by_default' => 'Hide media by default',
+    'copy_url_behavior' => 'Copy URL mode',
+    'settings_saved' => 'System settings saved!',
+    'export_data' => 'Export Data',
+    'password_recovery' => 'Recover password',
+    'no_account' => 'Don\'t have an account?',
+    'register' => 'Register',
+    'default_user_quota' => 'Default User Quota',
+    'invalid_quota' => 'Invalid values as default user quota.',
 ];

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

@@ -0,0 +1,7 @@
+ALTER TABLE `users`
+    ADD COLUMN `activate_token` VARCHAR(32) DEFAULT NULL,
+    ADD COLUMN `reset_token` VARCHAR(32) DEFAULT NULL,
+    ADD COLUMN `disk_quota` BIGINT(20) NOT NULL DEFAULT -1;
+
+ALTER TABLE `users` ADD INDEX (`activate_token`);
+ALTER TABLE `users` ADD INDEX (`reset_token`);

+ 10 - 0
resources/schemas/sqlite/sqlite.5.sql

@@ -0,0 +1,10 @@
+ALTER TABLE `users` ADD COLUMN `activate_token` VARCHAR(32);
+ALTER TABLE `users` ADD COLUMN `reset_token` VARCHAR(32);
+ALTER TABLE `users` ADD COLUMN `disk_quota` BIGINT NOT NULL DEFAULT -1;
+
+CREATE INDEX IF NOT EXISTS `activate_token_index`
+  ON `users` (`activate_token`);
+
+CREATE INDEX IF NOT EXISTS `reset_token_index`
+  ON `users` (`reset_token`);
+

+ 13 - 4
resources/templates/auth/login.twig

@@ -35,19 +35,28 @@
             </div>
             <div class="row">
                 <div class="col-md-12">
-                    <label for="inputEmail" class="sr-only">{{ lang('login.username') }}</label>
+                    <label for="username" class="sr-only">{{ lang('login.username') }}</label>
                     <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 class="d-flex justify-content-between">
+                        <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>
+                        <a href="#" class="">{{ lang('password_recovery') }}</a>
                     </div>
+
                 </div>
             </div>
             <div class="row mt-2">
                 <div class="col-md-12">
                     <button class="btn btn-lg btn-primary btn-block" type="submit">{{ lang('login') }}</button>
+                    {% if register_enabled == 'on' %}
+                    <div class="text-center mt-2">
+                        {{ lang('no_account') }} <a href="{{ route('register.show') }}">{{ lang('register') }}</a>.
+                    </div>
+                    {% endif %}
                 </div>
             </div>
         </form>

+ 56 - 0
resources/templates/auth/register.twig

@@ -0,0 +1,56 @@
+{% extends 'base.twig' %}
+
+{% block title %}{{ lang('login') }}{% endblock %}
+
+{% block head %}
+    <style>
+        html {
+            height: 100%;
+        }
+        body {
+            height: 100%;
+            display: -ms-flexbox;
+            display: -webkit-box;
+            display: flex;
+            -ms-flex-align: center;
+            -ms-flex-pack: center;
+            -webkit-box-align: center;
+            align-items: center;
+            -webkit-box-pack: center;
+            justify-content: center;
+            padding-bottom: 40px;
+            margin-bottom: 0;
+        }
+    </style>
+{% endblock %}
+
+{% block content %}
+    <div class="container-fluid">
+        <form class="form-signin" method="post" action="{{ route('register') }}">
+            <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' %}
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-md-12">
+                    <label for="username" class="sr-only">{{ lang('username') }}</label>
+                    <input type="text" id="username" class="form-control" placeholder="{{ lang('username') }}" name="username" required autofocus>
+                    <label for="email" class="sr-only">E-Mail</label>
+                    <input type="email" id="email" class="form-control" placeholder="mail@example.com" name="password" required>
+                    <label for="password" class="sr-only">{{ lang('password') }}</label>
+                    <input type="password" id="password" class="form-control" placeholder="{{ lang('password') }}" name="password" required>
+                </div>
+            </div>
+            <div class="row mt-2">
+                <div class="col-md-12">
+                    <button class="btn btn-lg btn-primary btn-block" type="submit">{{ lang('register') }}</button>
+                    <div class="text-center mt-2">
+                        <a href="{{ route('login.show') }}">{{ lang('cancel') }}</a>
+                    </div>
+                </div>
+            </div>
+        </form>
+    </div>
+{% endblock %}

+ 1 - 1
resources/templates/comp/navbar.twig

@@ -37,7 +37,7 @@
                     <div class="dropdown-menu shadow-sm" aria-labelledby="userDropdown">
                         <a class="dropdown-item disabled" href="javascript:void(0)">{{ lang('used') }}: {{ session.get('used_space') }}</a>
                         {% if session.get('admin') %}
-                            <a class="dropdown-item" href="{{ route('switchView') }}"><i class="fas fa-fw fa-sync"></i> {{ lang('switch_to') }}: {{ session.gallery_view is null or session.gallery_view ? lang('gallery') : lang('table') }}</a>
+                            <a class="dropdown-item" href="{{ route('switchView') }}"><i class="fas fa-fw fa-sync"></i> {{ lang('switch_to') }}: {{ session.get('gallery_view') is null or session.get('gallery_view') ? lang('gallery') : lang('table') }}</a>
                         {% endif %}
                         <div class="dropdown-divider"></div>
                         <a class="dropdown-item" href="{{ route('profile') }}"><i class="fas fa-fw fa-user"></i> {{ lang('profile') }}</a>

+ 7 - 0
resources/templates/dashboard/system.twig

@@ -90,6 +90,13 @@
                                 </div>
                             </div>
 
+                            <div class="form-group row">
+                                <label for="default_user_quota" class="col-sm-4 col-form-label">{{ lang('default_user_quota') }}</label>
+                                <div class="col-sm-8">
+                                    <input type="text" class="form-control" id="default_user_quota" name="default_user_quota" pattern="[0-9]+[K|M|G|T]" title="512M, 2G, 1T, ..." placeholder="1G" value="{{ default_user_quota }}">
+                                </div>
+                            </div>
+
                             <div class="form-group row">
                                 <label for="custom_head" class="col-sm-4 col-form-label">{{ lang('enforce_language') }}</label>
                                 <div class="col-sm-8">

+ 5 - 0
src/css/app.css

@@ -34,6 +34,11 @@ body {
     border-bottom-left-radius: 0;
 }
 
+.form-signin input[type="email"] {
+    margin-bottom: -1px;
+    border-radius: 0;
+}
+
 .form-signin input[type="password"] {
     margin-bottom: .50rem;
     border-top-left-radius: 0;

+ 1 - 1
src/js/app.js

@@ -106,7 +106,7 @@ var app = {
         window.open($('#telegram-share-button').data('url') + $('#telegram-share-text').val(), '_blank');
     },
     checkForUpdates: function () {
-        $('#checkForUpdatesMessage').empty().html('<i class="fas fa-sync fa-spin"></i>');
+        $('#checkForUpdatesMessage').empty().html('<i class="fas fa-spinner fa-pulse fa-3x"></i>');
         $('#doUpgradeButton').prop('disabled', true);
         $.get(window.AppConfig.base_url + '/system/checkForUpdates?prerelease=' + $(this).data('prerelease'), function (data) {
             $('#checkForUpdatesMessage').empty().text(data.message);