User registration procedure

This commit is contained in:
Sergio Brighenti 2020-02-27 15:18:01 +01:00
parent 3d8fee6f61
commit 0269eaa6f6
14 changed files with 247 additions and 89 deletions

View file

@ -37,6 +37,7 @@ class AdminController extends Controller
$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';
$quotaEnabled = $this->database->query('SELECT `value` FROM `settings` WHERE `key` = \'quota_enabled\'')->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', [
@ -53,7 +54,8 @@ class AdminController extends Controller
'register_enabled' => $registerEnabled,
'hide_by_default' => $hideByDefault,
'copy_url_behavior' => $copyUrl,
'default_user_quota' => $defaultUserQuota,
'quota_enabled' => $quotaEnabled,
'default_user_quota' => humanFileSize($defaultUserQuota, 0, true),
]);
}

View file

@ -4,6 +4,7 @@
namespace App\Controllers\Auth;
use App\Controllers\Controller;
use App\Web\Mail;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
@ -100,6 +101,18 @@ class RegisterController extends Controller
$activateToken,
]);
Mail::make()
->from('no-reply@'.str_ireplace('www.', '', parse_url($this->config['base_url'], PHP_URL_HOST)), $this->config['app_name'])
->to(param($request, 'email'))
->subject(lang('mail.activate_account', [$this->config['app_name']]))
->message(lang('mail.activate_text', [
param($request, 'username'),
$this->config['app_name'],
$this->config['base_url'],
route('activate', ['activateToken' => $activateToken]),
]))
->send();
$this->session->alert(lang('register_success', [param($request, 'username')]), 'success');
$this->logger->info('New user registered.', [array_diff_key($request->getParsedBody(), array_flip(['password']))]);
@ -114,5 +127,24 @@ class RegisterController extends Controller
*/
public function activateUser(Request $request, Response $response, string $activateToken): Response
{
if ($this->session->get('logged', false)) {
return redirect($response, route('home'));
}
$userId = $this->database->query('SELECT `id` FROM `users` WHERE `activate_token` = ? LIMIT 1', $activateToken)->fetch()->id ?? null;
if ($userId === null) {
$this->session->alert(lang('account_not_found'), 'warning');
return redirect($response, route('login.show'));
}
$this->database->query('UPDATE `users` SET `activate_token`=?, `active`=? WHERE `id` = ?', [
null,
1,
$userId,
]);
$this->session->alert(lang('account_activated'), 'success');
return redirect($response, route('login.show'));
}
}

View file

@ -23,7 +23,8 @@ class SettingController extends Controller
$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('quota_enabled', param($request, 'quota_enabled', 'off'));
$this->updateSetting('default_user_quota', stringToBytes(param($request, 'default_user_quota', '1G')));
$this->updateSetting('copy_url_behavior', param($request, 'copy_url_behavior') === null ? 'default' : 'raw');
$this->applyTheme($request);

127
app/Web/Mail.php Normal file
View file

@ -0,0 +1,127 @@
<?php
namespace App\Web;
use InvalidArgumentException;
class Mail
{
protected $fromMail = 'no-reply@example.com';
protected $fromName;
protected $to;
protected $subject;
protected $message;
protected $additionalHeaders = '';
protected $headers = '';
/**
* @return Mail
*/
public static function make()
{
return new self();
}
/**
* @param $mail
* @param $name
* @return $this
*/
public function from(string $mail, string $name)
{
$this->fromMail = $mail;
$this->fromName = $name;
return $this;
}
/**
* @param $mail
* @return $this
*/
public function to(string $mail)
{
if (!filter_var($mail, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Mail not valid.');
}
$this->to = $mail;
return $this;
}
/**
* @param $text
* @return $this
*/
public function subject(string $text)
{
$this->subject = htmlentities($text);
return $this;
}
/**
* @param $text
* @return $this
*/
public function message(string $text)
{
$this->message = htmlentities($text);
return $this;
}
/**
* @param $header
* @return $this
*/
public function addHeader(string $header)
{
$this->additionalHeaders .= "$header\r\n";
return $this;
}
/**
* @param $header
* @return $this
*/
protected function addRequiredHeader(string $header)
{
$this->headers .= "$header\r\n";
return $this;
}
/**
* @return int
*/
public function send()
{
if ($this->to === null) {
throw new InvalidArgumentException('Target email cannot be null.');
}
if ($this->subject === null) {
throw new InvalidArgumentException('Subject cannot be null.');
}
if ($this->message === null) {
throw new InvalidArgumentException('Message cannot be null.');
}
if ($this->fromName === null) {
$this->addRequiredHeader("From: $this->fromMail");
} else {
$this->addRequiredHeader("From: $this->fromName <$this->fromMail>");
}
$this->addRequiredHeader('X-Mailer: PHP/'.phpversion());
$this->addRequiredHeader('MIME-Version: 1.0');
$this->addRequiredHeader('Content-Type: text/html; charset=iso-8859-1');
$this->headers .= $this->additionalHeaders;
return (int) mail($this->to, $this->subject, $this->message, $this->headers);
}
}

View file

@ -13,22 +13,27 @@ if (!function_exists('humanFileSize')) {
* Generate a human readable file size.
*
* @param $size
* @param int $precision
* @param int $precision
*
* @param bool $iniMode
* @return string
*/
function humanFileSize($size, $precision = 2): string
function humanFileSize($size, $precision = 2, $iniMode = false): string
{
for ($i = 0; ($size / 1024) > 0.9; $i++, $size /= 1024) {
}
if ($iniMode) {
return round($size, $precision).['B', 'K', 'M', 'G', 'T'][$i];
}
return round($size, $precision).' '.['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'][$i];
}
}
if (!function_exists('humanRandomString')) {
/**
* @param int $length
* @param int $length
*
* @return string
*/
@ -49,7 +54,7 @@ if (!function_exists('humanRandomString')) {
if (!function_exists('isDisplayableImage')) {
/**
* @param string $mime
* @param string $mime
*
* @return bool
*/
@ -140,7 +145,7 @@ if (!function_exists('resolve')) {
/**
* Resolve a service from de DI container.
*
* @param string $service
* @param string $service
*
* @return mixed
*/
@ -168,9 +173,9 @@ if (!function_exists('redirect')) {
/**
* Set the redirect response.
*
* @param Response $response
* @param string $url
* @param int $status
* @param Response $response
* @param string $url
* @param int $status
*
* @return Response
*/
@ -186,7 +191,7 @@ if (!function_exists('asset')) {
/**
* Get the asset link with timestamp.
*
* @param string $path
* @param string $path
*
* @return string
*/
@ -200,8 +205,8 @@ if (!function_exists('urlFor')) {
/**
* Generate the app url given a path.
*
* @param string $path
* @param string $append
* @param string $path
* @param string $append
*
* @return string
*/
@ -217,9 +222,9 @@ if (!function_exists('route')) {
/**
* Generate the app url given a path.
*
* @param string $path
* @param array $args
* @param string $append
* @param string $path
* @param array $args
* @param string $append
*
* @return string
*/
@ -236,9 +241,9 @@ if (!function_exists('param')) {
/**
* Get a parameter from the request.
*
* @param Request $request
* @param string $name
* @param null $default
* @param Request $request
* @param string $name
* @param null $default
*
* @return string
*/
@ -262,10 +267,10 @@ if (!function_exists('json')) {
/**
* Return a json response.
*
* @param Response $response
* @param Response $response
* @param $data
* @param int $status
* @param int $options
* @param int $status
* @param int $options
*
* @return Response
*/
@ -281,8 +286,8 @@ if (!function_exists('json')) {
if (!function_exists('lang')) {
/**
* @param string $key
* @param array $args
* @param string $key
* @param array $args
*
* @return string
*/
@ -294,7 +299,7 @@ if (!function_exists('lang')) {
if (!function_exists('isBot')) {
/**
* @param string $userAgent
* @param string $userAgent
*
* @return bool
*/
@ -331,27 +336,27 @@ if (!function_exists('mime2font')) {
function mime2font($mime)
{
$classes = [
'image' => 'fa-file-image',
'audio' => 'fa-file-audio',
'video' => 'fa-file-video',
'application/pdf' => 'fa-file-pdf',
'application/msword' => 'fa-file-word',
'application/vnd.ms-word' => 'fa-file-word',
'application/vnd.oasis.opendocument.text' => 'fa-file-word',
'image' => 'fa-file-image',
'audio' => 'fa-file-audio',
'video' => 'fa-file-video',
'application/pdf' => 'fa-file-pdf',
'application/msword' => 'fa-file-word',
'application/vnd.ms-word' => 'fa-file-word',
'application/vnd.oasis.opendocument.text' => 'fa-file-word',
'application/vnd.openxmlformats-officedocument.wordprocessingml' => 'fa-file-word',
'application/vnd.ms-excel' => 'fa-file-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml' => 'fa-file-excel',
'application/vnd.oasis.opendocument.spreadsheet' => 'fa-file-excel',
'application/vnd.ms-powerpoint' => 'fa-file-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml' => 'fa-file-powerpoint',
'application/vnd.oasis.opendocument.presentation' => 'fa-file-powerpoint',
'text/plain' => 'fa-file-alt',
'text/html' => 'fa-file-code',
'text/x-php' => 'fa-file-code',
'application/json' => 'fa-file-code',
'application/gzip' => 'fa-file-archive',
'application/zip' => 'fa-file-archive',
'application/octet-stream' => 'fa-file-alt',
'application/vnd.ms-excel' => 'fa-file-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml' => 'fa-file-excel',
'application/vnd.oasis.opendocument.spreadsheet' => 'fa-file-excel',
'application/vnd.ms-powerpoint' => 'fa-file-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml' => 'fa-file-powerpoint',
'application/vnd.oasis.opendocument.presentation' => 'fa-file-powerpoint',
'text/plain' => 'fa-file-alt',
'text/html' => 'fa-file-code',
'text/x-php' => 'fa-file-code',
'application/json' => 'fa-file-code',
'application/gzip' => 'fa-file-archive',
'application/zip' => 'fa-file-archive',
'application/octet-stream' => 'fa-file-alt',
];
foreach ($classes as $fullMime => $class) {
@ -383,7 +388,7 @@ if (!function_exists('queryParams')) {
/**
* Get the query parameters of the current request.
*
* @param array $replace
* @param array $replace
*
* @return string
*/
@ -401,8 +406,8 @@ if (!function_exists('inPath')) {
/**
* Check if uri start with a path.
*
* @param string $uri
* @param string $path
* @param string $uri
* @param string $path
*
* @return bool
*/
@ -410,7 +415,7 @@ if (!function_exists('inPath')) {
{
$path = parse_url(urlFor($path), PHP_URL_PATH);
return substr($uri, 0, strlen($uri)) === $path;
return substr($uri, 0, strlen($path)) === $path;
}
}
@ -419,7 +424,7 @@ if (!function_exists('glob_recursive')) {
* Does not support flag GLOB_BRACE.
*
* @param $pattern
* @param int $flags
* @param int $flags
*
* @return array|false
*/

View file

@ -65,6 +65,7 @@ $app->group('', function (RouteCollectorProxy $group) {
$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('/activate/{activateToken}', [RegisterController::class, 'activateUser'])->setName('activate');
$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');

View file

@ -121,4 +121,5 @@ return [
'register' => 'Register',
'default_user_quota' => 'Default User Quota',
'invalid_quota' => 'Invalid values as default user quota.',
'mail.activate_text' => "Hi %s!\nthank you for creating your account on %s (%s), click on the following link to activate it:\n\n%s"
];

View file

@ -1,7 +1,8 @@
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;
ADD COLUMN `current_disk_quota` BIGINT(20) NOT NULL DEFAULT 0,
ADD COLUMN `max_disk_quota` BIGINT(20) NOT NULL DEFAULT -1;
ALTER TABLE `users` ADD INDEX (`activate_token`);
ALTER TABLE `users` ADD INDEX (`reset_token`);

View file

@ -1,6 +1,7 @@
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;
ALTER TABLE `users` ADD COLUMN `current_disk_quota` BIGINT NOT NULL DEFAULT 0;
ALTER TABLE `users` ADD COLUMN `max_disk_quota` BIGINT NOT NULL DEFAULT -1;
CREATE INDEX IF NOT EXISTS `activate_token_index`
ON `users` (`activate_token`);

View file

@ -38,7 +38,7 @@
<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>
<input type="email" id="email" class="form-control" placeholder="mail@example.com" name="email" 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>

View file

@ -18,7 +18,7 @@
</li>
{% if session.get('admin') %}
<li class="nav-item">
<a href="{{ route('user.index') }}" class="nav-link {{ inPath(request.uri.path, '/users') ? 'active' }}"><i class="fas fa-fw fa-users"></i>
<a href="{{ route('user.index') }}" class="nav-link {{ inPath(request.uri.path, '/user') ? 'active' }}"><i class="fas fa-fw fa-users"></i>
{{ lang('users') }}
</a>
</li>

View file

@ -61,28 +61,28 @@
<form method="post" action="{{ route('settings.save') }}">
<div class="form-group row">
<label for="custom_head" class="col-sm-4 col-form-label">{{ lang('register_enabled') }}</label>
<label for="register_enabled" class="col-sm-4 col-form-label">{{ lang('register_enabled') }}</label>
<div class="col-sm-8">
<input type="checkbox" name="register_enabled" data-toggle="toggle" {{ register_enabled == 'on' ? 'checked' }}>
</div>
</div>
<div class="form-group row">
<label for="custom_head" class="col-sm-4 col-form-label">{{ lang('hide_by_default') }}</label>
<label for="hide_by_default" class="col-sm-4 col-form-label">{{ lang('hide_by_default') }}</label>
<div class="col-sm-8">
<input type="checkbox" name="hide_by_default" data-toggle="toggle" {{ hide_by_default == 'on' ? 'checked' }}>
</div>
</div>
<div class="form-group row">
<label for="custom_head" class="col-sm-4 col-form-label">{{ lang('copy_url_behavior') }}</label>
<label for="copy_url_behavior" class="col-sm-4 col-form-label">{{ lang('copy_url_behavior') }}</label>
<div class="col-sm-8">
<input type="checkbox" name="copy_url_behavior" data-toggle="toggle" data-off="Default URL" data-on="Raw URL" data-onstyle="primary" data-offstyle="secondary" {{ copy_url_behavior == 'raw' ? 'checked' }}>
</div>
</div>
<div class="form-group row">
<label for="custom_head" class="col-sm-4 col-form-label">{{ lang('theme') }}</label>
<label for="themes" class="col-sm-4 col-form-label">{{ lang('theme') }}</label>
<div class="col-sm-8">
<select class="form-control" id="themes" name="css">
<option id="theme-load" selected disabled hidden>{{ lang('click_to_load') }}</option>
@ -90,6 +90,13 @@
</div>
</div>
<div class="form-group row">
<label for="quota_enabled" class="col-sm-4 col-form-label">{{ lang('quota_enabled') }}</label>
<div class="col-sm-8">
<input type="checkbox" name="quota_enabled" data-toggle="toggle" {{ quota_enabled == 'on' ? 'checked' }}>
</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">
@ -98,7 +105,7 @@
</div>
<div class="form-group row">
<label for="custom_head" class="col-sm-4 col-form-label">{{ lang('enforce_language') }}</label>
<label for="lang" class="col-sm-4 col-form-label">{{ lang('enforce_language') }}</label>
<div class="col-sm-8">
<select class="form-control" id="lang" name="lang">
<option value="auto">({{ lang('auto_set') }})</option>

View file

@ -7,7 +7,7 @@
<div class="container">
{% include 'comp/alert.twig' %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="col-md-10">
<div class="card shadow-sm">
<div class="card-header">{{ lang('user.create') }}</div>
<div class="card-body">
@ -31,25 +31,15 @@
</div>
</div>
<div class="form-group row">
<div class="col-sm-2"></div>
<label for="is_admin" class="col-sm-2 col-form-label">{{ lang('is_admin') }}</label>
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_admin" name="is_admin">
<label class="form-check-label" for="is_admin">
{{ lang('is_admin') }}
</label>
</div>
<input type="checkbox" name="is_admin" data-toggle="toggle" data-off="{{ lang('no') }}" data-on="{{ lang('yes') }}">
</div>
</div>
<div class="form-group row">
<div class="col-sm-2"></div>
<label for="is_active" class="col-sm-2 col-form-label">{{ lang('is_active') }}</label>
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_active" name="is_active" checked>
<label class="form-check-label" for="is_active">
{{ lang('is_active') }}
</label>
</div>
<input type="checkbox" name="is_active" data-toggle="toggle" data-off="{{ lang('no') }}" data-on="{{ lang('yes') }}" checked>
</div>
</div>
<div class="form-group row justify-content-md-end">

View file

@ -7,7 +7,7 @@
<div class="container">
{% include 'comp/alert.twig' %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="col-md-10">
<div class="card shadow-sm">
{% if not profile %}
<div class="card-header">{{ lang('user.edit') }}</div>
@ -67,25 +67,15 @@
</div>
{% if not profile %}
<div class="form-group row">
<div class="col-sm-2"></div>
<label for="is_admin" class="col-sm-2 col-form-label">{{ lang('is_admin') }}</label>
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_admin" name="is_admin" {{ user.is_admin ? 'checked' }}>
<label class="form-check-label" for="is_admin">
{{ lang('is_admin') }}
</label>
</div>
<input type="checkbox" name="is_admin" data-toggle="toggle" data-off="{{ lang('no') }}" data-on="{{ lang('yes') }}" {{ user.is_admin ? 'checked' }}>
</div>
</div>
<div class="form-group row">
<div class="col-sm-2"></div>
<label for="is_active" class="col-sm-2 col-form-label">{{ lang('is_active') }}</label>
<div class="col-sm-10">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_active" name="is_active" {{ user.active ? 'checked' }}>
<label class="form-check-label" for="is_active">
{{ lang('is_active') }}
</label>
</div>
<input type="checkbox" name="is_active" data-toggle="toggle" data-off="{{ lang('no') }}" data-on="{{ lang('yes') }}" {{ user.active ? 'checked' }}>
</div>
</div>
{% endif %}