Added recaptcha verification option.
Added bulk delete function. Added account clean function (closes #151)
This commit is contained in:
parent
a22f6afc68
commit
e5d70c00ce
20 changed files with 209 additions and 43 deletions
|
@ -5,6 +5,10 @@
|
|||
+ Added ability to choose between default and raw url on copy.
|
||||
+ Added hide by default option.
|
||||
+ Added user disk quota.
|
||||
+ Added reCAPTCHA login protection.
|
||||
+ Added bulk delete.
|
||||
+ Added account clean function.
|
||||
+ Added user disk quota system.
|
||||
+ Fixed bug html files raws are rendered in a browser.
|
||||
+ The theme is now re-applied after every system update.
|
||||
+ Updated system settings page.
|
||||
|
|
|
@ -37,6 +37,9 @@ class AdminController extends Controller
|
|||
'copy_url_behavior' => $this->getSetting('copy_url_behavior', 'off'),
|
||||
'quota_enabled' => $this->getSetting('quota_enabled', 'off'),
|
||||
'default_user_quota' => humanFileSize($this->getSetting('default_user_quota', stringToBytes('1G')), 0, true),
|
||||
'recaptcha_enabled' => $this->getSetting('recaptcha_enabled', 'off'),
|
||||
'recaptcha_site_key' => $this->getSetting('recaptcha_site_key'),
|
||||
'recaptcha_secret_key' => $this->getSetting('recaptcha_secret_key'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Controllers\Auth;
|
||||
|
||||
use App\Controllers\Controller;
|
||||
use App\Web\ValidationChecker;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
|
@ -25,6 +26,7 @@ class LoginController extends Controller
|
|||
|
||||
return view()->render($response, 'auth/login.twig', [
|
||||
'register_enabled' => $this->getSetting('register_enabled', 'off'),
|
||||
'recaptcha_site_key' => $this->getSetting('recaptcha_enabled') === 'on' ? $this->getSetting('recaptcha_site_key') : null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -38,21 +40,34 @@ class LoginController extends Controller
|
|||
*/
|
||||
public function login(Request $request, Response $response): Response
|
||||
{
|
||||
if ($this->getSetting('recaptcha_enabled') === 'on') {
|
||||
$recaptcha = json_decode(file_get_contents('https://www.google.com/recaptcha/api/siteverify?secret='.$this->getSetting('recaptcha_secret_key').'&response='.param($request, 'recaptcha_token')));
|
||||
|
||||
if ($recaptcha->success && $recaptcha->score < 0.5) {
|
||||
$this->session->alert(lang('recaptcha_failed'), 'danger');
|
||||
return redirect($response, route('login'));
|
||||
}
|
||||
}
|
||||
|
||||
$username = param($request, 'username');
|
||||
$user = $this->database->query('SELECT `id`, `email`, `username`, `password`,`is_admin`, `active`, `current_disk_quota`, `max_disk_quota` FROM `users` WHERE `username` = ? OR `email` = ? LIMIT 1', [$username, $username])->fetch();
|
||||
|
||||
if (!$user || !password_verify(param($request, 'password'), $user->password)) {
|
||||
$this->session->alert(lang('bad_login'), 'danger');
|
||||
return redirect($response, route('login'));
|
||||
}
|
||||
$validator = ValidationChecker::make()
|
||||
->rules([
|
||||
'login' => $user && password_verify(param($request, 'password'), $user->password),
|
||||
'maintenance' => !isset($this->config['maintenance']) || !$this->config['maintenance'] || $user->is_admin,
|
||||
'user_active' => $user->active,
|
||||
])
|
||||
->onFail(function ($rule) {
|
||||
$alerts = [
|
||||
'login' => lang('bad_login'),
|
||||
'maintenance' => lang('maintenance_in_progress'),
|
||||
'user_active' => lang('account_disabled'),
|
||||
];
|
||||
|
||||
if (isset($this->config['maintenance']) && $this->config['maintenance'] && !$user->is_admin) {
|
||||
$this->session->alert(lang('maintenance_in_progress'), 'info');
|
||||
return redirect($response, route('login'));
|
||||
}
|
||||
|
||||
if (!$user->active) {
|
||||
$this->session->alert(lang('account_disabled'), 'danger');
|
||||
$this->session->alert($alerts[$rule], $rule === 'maintenance' ? 'info' : 'danger');
|
||||
});
|
||||
if ($validator->fails()) {
|
||||
return redirect($response, route('login'));
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,9 @@ class RegisterController extends Controller
|
|||
throw new HttpNotFoundException($request);
|
||||
}
|
||||
|
||||
return view()->render($response, 'auth/register.twig');
|
||||
return view()->render($response, 'auth/register.twig', [
|
||||
'recaptcha_site_key' => $this->getSetting('recaptcha_enabled') === 'on' ? $this->getSetting('recaptcha_site_key') : null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -52,6 +54,15 @@ class RegisterController extends Controller
|
|||
throw new HttpNotFoundException($request);
|
||||
}
|
||||
|
||||
if ($this->getSetting('recaptcha_enabled') === 'on') {
|
||||
$recaptcha = json_decode(file_get_contents('https://www.google.com/recaptcha/api/siteverify?secret='.$this->getSetting('recaptcha_secret_key').'&response='.param($request, 'recaptcha_token')));
|
||||
|
||||
if ($recaptcha->success && $recaptcha->score < 0.5) {
|
||||
$this->session->alert(lang('recaptcha_failed'), 'danger');
|
||||
return redirect($response, route('login'));
|
||||
}
|
||||
}
|
||||
|
||||
$validator = $this->getUserCreateValidator($request);
|
||||
|
||||
if ($validator->fails()) {
|
||||
|
|
|
@ -185,10 +185,8 @@ class MediaController extends Controller
|
|||
* @param int $id
|
||||
*
|
||||
* @return Response
|
||||
* @throws HttpUnauthorizedException
|
||||
*
|
||||
* @throws HttpNotFoundException
|
||||
* @throws FileNotFoundException
|
||||
* @throws HttpUnauthorizedException
|
||||
*/
|
||||
public function delete(Request $request, Response $response, int $id): Response
|
||||
{
|
||||
|
@ -199,11 +197,12 @@ class MediaController extends Controller
|
|||
}
|
||||
|
||||
if ($this->session->get('admin', false) || $media->user_id === $this->session->get('user_id')) {
|
||||
$size = $this->deleteMedia($request, $media->storage_path, $id);
|
||||
$this->updateUserQuota($request, $media->user_id, $size, true);
|
||||
$this->deleteMedia($request, $media->storage_path, $id, $media->user_id);
|
||||
|
||||
$this->logger->info('User '.$this->session->get('username').' deleted a media.', [$id]);
|
||||
|
||||
if ($media->user_id === $this->session->get('user_id')) {
|
||||
$user = make(UserQuery::class)->get($request, $id, true);
|
||||
$user = make(UserQuery::class)->get($request, $media->user_id, true);
|
||||
$this->setSessionQuotaInfo($user->current_disk_quota, $user->max_disk_quota);
|
||||
}
|
||||
} else {
|
||||
|
@ -252,8 +251,7 @@ class MediaController extends Controller
|
|||
}
|
||||
|
||||
if ($this->session->get('admin', false) || $user->id === $media->user_id) {
|
||||
$size = $this->deleteMedia($request, $media->storage_path, $media->mediaId);
|
||||
$this->updateUserQuota($request, $media->user_id, $size, true);
|
||||
$this->deleteMedia($request, $media->storage_path, $media->mediaId, $user->id);
|
||||
$this->logger->info('User '.$user->username.' deleted a media via token.', [$media->mediaId]);
|
||||
} else {
|
||||
throw new HttpUnauthorizedException($request);
|
||||
|
@ -262,20 +260,50 @@ class MediaController extends Controller
|
|||
return redirect($response, route('home'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function clearUserMedia(Request $request, Response $response, int $id): Response
|
||||
{
|
||||
$user = make(UserQuery::class)->get($request, $id, true);
|
||||
|
||||
$medias = $this->database->query('SELECT * FROM `uploads` WHERE `user_id` = ?', $user->id);
|
||||
|
||||
foreach ($medias as $media) {
|
||||
try {
|
||||
$this->storage->delete($media->storage_path);
|
||||
} catch (FileNotFoundException $e) {
|
||||
}
|
||||
}
|
||||
|
||||
$this->database->query('DELETE FROM `uploads` WHERE `user_id` = ?', $user->id);
|
||||
$this->database->query('UPDATE `users` SET `current_disk_quota`=? WHERE `id` = ?', [
|
||||
0,
|
||||
$user->id,
|
||||
]);
|
||||
|
||||
$this->session->alert(lang('account_media_deleted'), 'success');
|
||||
return redirect($response, route('user.edit', ['id' => $id]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param string $storagePath
|
||||
* @param int $id
|
||||
*
|
||||
* @return bool|false|int
|
||||
* @param int $userId
|
||||
* @return void
|
||||
* @throws HttpNotFoundException
|
||||
*/
|
||||
protected function deleteMedia(Request $request, string $storagePath, int $id)
|
||||
protected function deleteMedia(Request $request, string $storagePath, int $id, int $userId)
|
||||
{
|
||||
try {
|
||||
$size = $this->storage->getSize($storagePath);
|
||||
$this->storage->delete($storagePath);
|
||||
return $size;
|
||||
$this->updateUserQuota($request, $userId, $size, true);
|
||||
} catch (FileNotFoundException $e) {
|
||||
throw new HttpNotFoundException($request);
|
||||
} finally {
|
||||
|
@ -293,12 +321,10 @@ class MediaController extends Controller
|
|||
{
|
||||
$mediaCode = pathinfo($mediaCode)['filename'];
|
||||
|
||||
$media = $this->database->query('SELECT `uploads`.*, `users`.*, `users`.`id` AS `userId`, `uploads`.`id` AS `mediaId` FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_code` = ? AND `uploads`.`code` = ? LIMIT 1', [
|
||||
return $this->database->query('SELECT `uploads`.*, `users`.*, `users`.`id` AS `userId`, `uploads`.`id` AS `mediaId` FROM `uploads` INNER JOIN `users` ON `uploads`.`user_id` = `users`.`id` WHERE `user_code` = ? AND `uploads`.`code` = ? LIMIT 1', [
|
||||
$userCode,
|
||||
$mediaCode,
|
||||
])->fetch();
|
||||
|
||||
return $media;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,6 +22,11 @@ class SettingController extends Controller
|
|||
return redirect($response, route('system'));
|
||||
}
|
||||
|
||||
if (param($request, 'recaptcha_enabled', 'off') === 'on' && (empty(param($request, 'recaptcha_site_key')) || empty(param($request, 'recaptcha_secret_key')))) {
|
||||
$this->session->alert(lang('recaptcha_keys_required', '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('quota_enabled', param($request, 'quota_enabled', 'off'));
|
||||
|
@ -36,6 +41,11 @@ class SettingController extends Controller
|
|||
$this->applyLang($request);
|
||||
$this->updateSetting('custom_head', param($request, 'custom_head'));
|
||||
|
||||
|
||||
$this->updateSetting('recaptcha_enabled', param($request, 'recaptcha_enabled', 'off'));
|
||||
$this->updateSetting('recaptcha_site_key', param($request, 'recaptcha_site_key'));
|
||||
$this->updateSetting('recaptcha_secret_key', param($request, 'recaptcha_secret_key'));
|
||||
|
||||
$this->session->alert(lang('settings_saved'));
|
||||
|
||||
return redirect($response, route('system'));
|
||||
|
|
|
@ -41,6 +41,7 @@ class ViewFactory
|
|||
$twig->addFunction(new TwigFunction('isDisplayableImage', 'isDisplayableImage'));
|
||||
$twig->addFunction(new TwigFunction('inPath', 'inPath'));
|
||||
$twig->addFunction(new TwigFunction('humanFileSize', 'humanFileSize'));
|
||||
$twig->addFunction(new TwigFunction('param', 'param'));
|
||||
|
||||
return new View($twig);
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ $app->group('', function (RouteCollectorProxy $group) {
|
|||
$group->get('/{id}/edit', [UserController::class, 'edit'])->setName('user.edit');
|
||||
$group->post('/{id}', [UserController::class, 'update'])->setName('user.update');
|
||||
$group->get('/{id}/delete', [UserController::class, 'delete'])->setName('user.delete');
|
||||
$group->get('/{id}/clear', [MediaController::class, 'clearUserMedia'])->setName('user.clear');
|
||||
})->add(AdminMiddleware::class);
|
||||
|
||||
$group->get('/profile', [ProfileController::class, 'profile'])->setName('profile');
|
||||
|
|
|
@ -143,4 +143,15 @@ If it wasn\'t you who requested the password reset, simply ignore this email.',
|
|||
'recalculate_user_quota' => 'Recalculate user quota from disk',
|
||||
'quota_recalculated' => 'User quota recalculated from the disk successfully.',
|
||||
'used_space' => 'Used Space',
|
||||
'delete_selected' => 'Delete Selected',
|
||||
'delete_all' => 'Delete All',
|
||||
'clear_account' => 'Clear Account',
|
||||
'account_media_deleted' => 'All media in the account have been deleted.',
|
||||
'danger_zone' => 'Danger Zone',
|
||||
'recaptcha_failed' => 'reCAPTCHA Failed',
|
||||
'recaptcha_enabled' => 'reCAPTCHA Enabled',
|
||||
'recaptcha_keys_required' => 'All reCAPTCHA keys are required.',
|
||||
'only_recaptcha_v3' => 'Only reCAPTCHA v3 is supported.',
|
||||
'recaptcha_site_key' => 'reCAPTCHA Site Key.',
|
||||
'recaptcha_secret_key' => 'reCAPTCHA Secret Key.',
|
||||
];
|
||||
|
|
|
@ -35,6 +35,9 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% if recaptcha_site_key is not null %}
|
||||
<input type="hidden" name="recaptcha_token" id="recaptcha_token">
|
||||
{% endif %}
|
||||
<label for="username" class="sr-only">{{ lang('login.username') }}</label>
|
||||
<input type="text" id="username" class="form-control first" placeholder="{{ lang('login.username') }}" name="username" required autofocus>
|
||||
<label for="password" class="sr-only">{{ lang('password') }}</label>
|
||||
|
@ -60,4 +63,10 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{% if recaptcha_site_key is not null %}
|
||||
{% include 'comp/recaptcha.twig' %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -35,6 +35,9 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% if recaptcha_site_key is not null %}
|
||||
<input type="hidden" name="recaptcha_token" id="recaptcha_token">
|
||||
{% endif %}
|
||||
<label for="username" class="sr-only">{{ lang('username') }}</label>
|
||||
<input type="text" id="username" class="form-control first" placeholder="{{ lang('username') }}" name="username" required autofocus>
|
||||
<label for="email" class="sr-only">E-Mail</label>
|
||||
|
@ -53,4 +56,10 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{% if recaptcha_site_key is not null %}
|
||||
{% include 'comp/recaptcha.twig' %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -53,5 +53,6 @@
|
|||
<script src="{{ asset('/static/bootstrap/js/bootstrap4-toggle.min.js') }}"></script>
|
||||
<script src="{{ asset('/static/app/app.js') }}"></script>
|
||||
<script>hljs.initHighlightingOnLoad();</script>
|
||||
{% block js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
8
resources/templates/comp/recaptcha.twig
Normal file
8
resources/templates/comp/recaptcha.twig
Normal file
|
@ -0,0 +1,8 @@
|
|||
<script src="https://www.google.com/recaptcha/api.js?render={{ recaptcha_site_key }}"></script>
|
||||
<script>
|
||||
grecaptcha.ready(function () {
|
||||
grecaptcha.execute('{{ recaptcha_site_key }}', {action: 'auth'}).then(function (token) {
|
||||
$('#recaptcha_token').val(token);
|
||||
});
|
||||
});
|
||||
</script>
|
|
@ -10,7 +10,7 @@
|
|||
{% if medias|length > 0 %}
|
||||
<div class="row">
|
||||
{% for media in medias %}
|
||||
<div class="col-md-4" id="media_{{ media.id }}">
|
||||
<div class="col-md-4 bulk-selector" id="media_{{ media.id }}" data-id="{{ media.id }}">
|
||||
<div class="card mb-4 shadow-sm">
|
||||
{% if isDisplayableImage(media.mimetype) %}
|
||||
<img class="card-img" src="{{ urlFor('/' ~ media.user_code ~ '/' ~ media.code ~ '.' ~ media.extension ~ '/raw?width=348&height=267') }}" alt="Card image">
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
{% for media in medias %}
|
||||
<tr id="media_{{ media.id }}">
|
||||
<tr id="media_{{ media.id }}" class="bulk-selector" data-id="{{ media.id }}">
|
||||
<td class="text-center">
|
||||
{% if isDisplayableImage(media.mimetype) %}
|
||||
{% if media.username is not null %}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="col-md-3">
|
||||
<form method="get" action="{{ route('home') }}">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" name="search" class="form-control" placeholder="{{ lang('dotted_search') }}" aria-label="{{ lang('dotted_search') }}" value="{{ request.param('search', '') }}">
|
||||
<input type="text" name="search" class="form-control" placeholder="{{ lang('dotted_search') }}" aria-label="{{ lang('dotted_search') }}" value="{{ param(request, 'search', '') }}">
|
||||
<div class="input-group-append">
|
||||
<button type="submit" class="btn btn-outline-secondary"><i class="fas fa-search"></i></button>
|
||||
</div>
|
||||
|
@ -28,5 +28,6 @@
|
|||
<i class="fas {{ request.queryParams['order'] is same as('ASC') ? 'fa-sort-amount-up' : 'fa-sort-amount-down' }}"></i>
|
||||
</a>
|
||||
</div>
|
||||
<a href="javascript:void(0)" id="bulk-delete" class="btn btn-outline-danger disabled" data-toggle="tooltip" title="{{ lang('delete_selected') }}"><i class="fas fa-trash"></i></a>
|
||||
</div>
|
||||
</div>
|
|
@ -85,19 +85,6 @@
|
|||
</select>
|
||||
</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">
|
||||
<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 }}">
|
||||
<small>512M, 2G, 1T, ...</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="lang" class="col-sm-4 col-form-label">{{ lang('enforce_language') }}</label>
|
||||
<div class="col-sm-8">
|
||||
|
@ -117,6 +104,40 @@
|
|||
<small>{{ lang('custom_head_html_hint') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<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' }} onchange="document.getElementById('default_user_quota').toggleAttribute('readonly')">
|
||||
</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 }}" {{ quota_enabled == 'off' ? 'readonly' }}>
|
||||
<small>512M, 2G, 1T, ...</small>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group row">
|
||||
<label for="recaptcha_enabled" class="col-sm-4 col-form-label">{{ lang('recaptcha_enabled') }}</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="checkbox" name="recaptcha_enabled" data-toggle="toggle" {{ recaptcha_enabled == 'on' ? 'checked' }} onchange="document.getElementById('recaptcha_site_key').toggleAttribute('readonly');document.getElementById('recaptcha_secret_key').toggleAttribute('readonly')">
|
||||
<br><small>{{ lang('only_recaptcha_v3') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="recaptcha_site_key" class="col-sm-4 col-form-label">{{ lang('recaptcha_site_key') }}</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control" id="recaptcha_site_key" name="recaptcha_site_key" value="{{ recaptcha_site_key }}" {{ recaptcha_enabled == 'off' ? 'readonly' }}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="recaptcha_secret_key" class="col-sm-4 col-form-label">{{ lang('recaptcha_secret_key') }}</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control" id="recaptcha_secret_key" name="recaptcha_secret_key" value="{{ recaptcha_secret_key }}" {{ recaptcha_enabled == 'off' ? 'readonly' }}>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline-success float-right mt-3">
|
||||
<i class="fas fa-save fa-fw"></i> {{ lang('apply') }}
|
||||
</button>
|
||||
|
|
|
@ -66,6 +66,8 @@
|
|||
</div>
|
||||
</div>
|
||||
{% if not profile %}
|
||||
<h6 class="text-danger">{{ lang('danger_zone') }}</h6>
|
||||
<hr>
|
||||
{% if quota_enabled == 'on' %}
|
||||
<div class="form-group row">
|
||||
<label for="max_user_quota" class="col-sm-2 col-form-label">{{ lang('max_user_quota') }}</label>
|
||||
|
@ -87,6 +89,14 @@
|
|||
<input type="checkbox" name="is_active" data-toggle="toggle" data-off="{{ lang('no') }}" data-on="{{ lang('yes') }}" {{ user.active ? 'checked' }}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">{{ lang('delete_all') }}</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="btn-group">
|
||||
<a href="{{ route('user.clear', {'id': user.id}) }}" class="btn btn-lg btn-outline-danger"><i class="fas fa-fw fa-recycle"></i> {{ lang('clear_account') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-group row justify-content-md-end">
|
||||
<div class="col-sm-10">
|
||||
|
|
|
@ -125,4 +125,8 @@ body {
|
|||
.pdf-viewer {
|
||||
width: 100%;
|
||||
height: 85vh
|
||||
}
|
||||
}
|
||||
|
||||
.grecaptcha-badge {
|
||||
top: 5px !important;
|
||||
}
|
||||
|
|
|
@ -25,6 +25,9 @@ var app = {
|
|||
$('#themes').mousedown(app.loadThemes);
|
||||
$('.checkForUpdatesButton').click(app.checkForUpdates);
|
||||
|
||||
$('.bulk-selector').contextmenu(app.bulkSelect);
|
||||
$('#bulk-delete').click(app.bulkDelete);
|
||||
|
||||
$('.alert').fadeTo(10000, 500).slideUp(500, function () {
|
||||
$('.alert').slideUp(500);
|
||||
});
|
||||
|
@ -116,6 +119,24 @@ var app = {
|
|||
$('#doUpgradeButton').prop('disabled', true);
|
||||
}
|
||||
});
|
||||
},
|
||||
bulkSelect: function (e) {
|
||||
e.preventDefault();
|
||||
$(this).toggleClass('bg-light').toggleClass('text-danger').toggleClass('bulk-selected');
|
||||
var $bulkDelete = $('#bulk-delete');
|
||||
if ($bulkDelete.hasClass('disabled')) {
|
||||
$bulkDelete.removeClass('disabled');
|
||||
}
|
||||
},
|
||||
bulkDelete: function () {
|
||||
$('.bulk-selected').each(function (index, media) {
|
||||
$.post(window.AppConfig.base_url + '/upload/' + $(media).data('id') + '/delete', function () {
|
||||
$(media).fadeOut(200, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
});
|
||||
$(this).addClass('disabled');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue