User: Started cleanup of user self-management

- Moved preference views to more general "my-account" area.
- Started new layout for my-account with sidebar.
- Added MFA to prefeences view (to be moved).
This commit is contained in:
Dan Brown 2023-10-17 13:11:10 +01:00
parent 3274181e14
commit a9d0f36766
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
13 changed files with 307 additions and 245 deletions

View File

@ -0,0 +1,105 @@
<?php
namespace BookStack\Users\Controllers;
use BookStack\Http\Controller;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Settings\UserNotificationPreferences;
use BookStack\Settings\UserShortcutMap;
use BookStack\Users\UserRepo;
use Illuminate\Http\Request;
class UserAccountController extends Controller
{
public function __construct(
protected UserRepo $userRepo
) {
}
/**
* Show the overview for user preferences.
*/
public function index()
{
$guest = user()->isGuest();
$mfaMethods = $guest ? [] : user()->mfaValues->groupBy('method');
return view('users.account.index', [
'mfaMethods' => $mfaMethods,
]);
}
/**
* Show the user-specific interface shortcuts.
*/
public function showShortcuts()
{
$shortcuts = UserShortcutMap::fromUserPreferences();
$enabled = setting()->getForCurrentUser('ui-shortcuts-enabled', false);
$this->setPageTitle(trans('preferences.shortcuts_interface'));
return view('users.account.shortcuts', [
'shortcuts' => $shortcuts,
'enabled' => $enabled,
]);
}
/**
* Update the user-specific interface shortcuts.
*/
public function updateShortcuts(Request $request)
{
$enabled = $request->get('enabled') === 'true';
$providedShortcuts = $request->get('shortcut', []);
$shortcuts = new UserShortcutMap($providedShortcuts);
setting()->putForCurrentUser('ui-shortcuts', $shortcuts->toJson());
setting()->putForCurrentUser('ui-shortcuts-enabled', $enabled);
$this->showSuccessNotification(trans('preferences.shortcuts_update_success'));
return redirect('/my-account/shortcuts');
}
/**
* Show the notification preferences for the current user.
*/
public function showNotifications(PermissionApplicator $permissions)
{
$this->checkPermission('receive-notifications');
$this->preventGuestAccess();
$preferences = (new UserNotificationPreferences(user()));
$query = user()->watches()->getQuery();
$query = $permissions->restrictEntityRelationQuery($query, 'watches', 'watchable_id', 'watchable_type');
$query = $permissions->filterDeletedFromEntityRelationQuery($query, 'watches', 'watchable_id', 'watchable_type');
$watches = $query->with('watchable')->paginate(20);
$this->setPageTitle(trans('preferences.notifications'));
return view('users.account.notifications', [
'preferences' => $preferences,
'watches' => $watches,
]);
}
/**
* Update the notification preferences for the current user.
*/
public function updateNotifications(Request $request)
{
$this->checkPermission('receive-notifications');
$this->preventGuestAccess();
$data = $this->validate($request, [
'preferences' => ['required', 'array'],
'preferences.*' => ['required', 'string'],
]);
$preferences = (new UserNotificationPreferences(user()));
$preferences->updateFromSettingsArray($data['preferences']);
$this->showSuccessNotification(trans('preferences.notifications_update_success'));
return redirect('/my-account/notifications');
}
}

View File

@ -16,88 +16,6 @@ class UserPreferencesController extends Controller
) {
}
/**
* Show the overview for user preferences.
*/
public function index()
{
return view('users.preferences.index');
}
/**
* Show the user-specific interface shortcuts.
*/
public function showShortcuts()
{
$shortcuts = UserShortcutMap::fromUserPreferences();
$enabled = setting()->getForCurrentUser('ui-shortcuts-enabled', false);
$this->setPageTitle(trans('preferences.shortcuts_interface'));
return view('users.preferences.shortcuts', [
'shortcuts' => $shortcuts,
'enabled' => $enabled,
]);
}
/**
* Update the user-specific interface shortcuts.
*/
public function updateShortcuts(Request $request)
{
$enabled = $request->get('enabled') === 'true';
$providedShortcuts = $request->get('shortcut', []);
$shortcuts = new UserShortcutMap($providedShortcuts);
setting()->putForCurrentUser('ui-shortcuts', $shortcuts->toJson());
setting()->putForCurrentUser('ui-shortcuts-enabled', $enabled);
$this->showSuccessNotification(trans('preferences.shortcuts_update_success'));
return redirect('/preferences/shortcuts');
}
/**
* Show the notification preferences for the current user.
*/
public function showNotifications(PermissionApplicator $permissions)
{
$this->checkPermission('receive-notifications');
$this->preventGuestAccess();
$preferences = (new UserNotificationPreferences(user()));
$query = user()->watches()->getQuery();
$query = $permissions->restrictEntityRelationQuery($query, 'watches', 'watchable_id', 'watchable_type');
$query = $permissions->filterDeletedFromEntityRelationQuery($query, 'watches', 'watchable_id', 'watchable_type');
$watches = $query->with('watchable')->paginate(20);
$this->setPageTitle(trans('preferences.notifications'));
return view('users.preferences.notifications', [
'preferences' => $preferences,
'watches' => $watches,
]);
}
/**
* Update the notification preferences for the current user.
*/
public function updateNotifications(Request $request)
{
$this->checkPermission('receive-notifications');
$this->preventGuestAccess();
$data = $this->validate($request, [
'preferences' => ['required', 'array'],
'preferences.*' => ['required', 'string'],
]);
$preferences = (new UserNotificationPreferences(user()));
$preferences->updateFromSettingsArray($data['preferences']);
$this->showSuccessNotification(trans('preferences.notifications_update_success'));
return redirect('/preferences/notifications');
}
/**
* Update the preferred view format for a list view of the given type.
*/

View File

@ -5,10 +5,10 @@
*/
return [
'preferences' => 'Preferences',
'my_account' => 'My Account',
'shortcuts' => 'Shortcuts',
'shortcuts_interface' => 'Interface Keyboard Shortcuts',
'shortcuts_interface' => 'UI Shortcut Preferences',
'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@ -35,9 +35,9 @@
</li>
<li><hr></li>
<li>
<a href="{{ url('/preferences') }}" class="icon-item">
<a href="{{ url('/my-account') }}" class="icon-item">
@icon('user-preferences')
<div>{{ trans('preferences.preferences') }}</div>
<div>{{ trans('preferences.my_account') }}</div>
</a>
</li>
<li>

View File

@ -9,7 +9,7 @@
<p class="text-muted">{{ trans('preferences.shortcuts_overview_desc') }}</p>
</div>
<div class="text-right">
<a href="{{ url('/preferences/shortcuts') }}" class="button outline">{{ trans('common.manage') }}</a>
<a href="{{ url('/my-account/shortcuts') }}" class="button outline">{{ trans('common.manage') }}</a>
</div>
</section>
@ -20,7 +20,7 @@
<p class="text-muted">{{ trans('preferences.notifications_desc') }}</p>
</div>
<div class="text-right">
<a href="{{ url('/preferences/notifications') }}" class="button outline">{{ trans('common.manage') }}</a>
<a href="{{ url('/my-account/notifications') }}" class="button outline">{{ trans('common.manage') }}</a>
</div>
</section>
@endif
@ -37,5 +37,26 @@
</section>
@endif
@if(!user()->isGuest())
<section class="card content-wrap auto-height items-center flex-container-row gap-m gap-x-l wrap justify-space-between">
<div class="flex-min-width-m">
<h2 class="list-heading">{{ trans('settings.users_mfa') }}</h2>
<p class="text-muted">{{ trans('settings.users_mfa_desc') }}</p>
<p class="text-muted">
@if ($mfaMethods->count() > 0)
<span class="text-pos">@icon('check-circle')</span>
@else
<span class="text-neg">@icon('cancel')</span>
@endif
{{ trans_choice('settings.users_mfa_x_methods', $mfaMethods->count()) }}
</p>
</div>
<div class="text-right">
<a href="{{ url('/mfa/setup') }}"
class="button outline">{{ trans('common.manage') }}</a>
</div>
</section>
@endif
</div>
@stop

View File

@ -0,0 +1,26 @@
@extends('layouts.simple')
@section('body')
<div class="container medium">
<div class="grid gap-xxl right-focus my-xl">
<div>
<div class="sticky-top-m">
<h5>{{ trans('preferences.my_account') }}</h5>
<nav class="active-link-list in-sidebar">
<a href="{{ url('/my-account/shortcuts') }}" class="{{ 'shortcuts' === 'shortcuts' ? 'active' : '' }}">@icon('shortcuts') {{ trans('preferences.shortcuts_interface') }}</a>
<a href="{{ url('/my-account/notifications') }}" class="{{ '' === 'notifications' ? 'active' : '' }}">@icon('notifications') {{ trans('preferences.notifications') }}</a>
<a href="{{ url('/my-account/auth') }}" class="{{ '' === 'auth' ? 'active' : '' }}">@icon('lock') {{ 'Access & Security' }}</a>
</nav>
</div>
</div>
<div>
@yield('main')
</div>
</div>
</div>
@stop

View File

@ -0,0 +1,71 @@
@extends('users.account.layout')
@section('main')
<section class="card content-wrap auto-height">
<form action="{{ url('/my-account/notifications') }}" method="post">
{{ method_field('put') }}
{{ csrf_field() }}
<h1 class="list-heading">{{ trans('preferences.notifications') }}</h1>
<p class="text-small text-muted">{{ trans('preferences.notifications_desc') }}</p>
<div class="flex-container-row wrap justify-space-between pb-m">
<div class="toggle-switch-list min-width-l">
<div>
@include('form.toggle-switch', [
'name' => 'preferences[own-page-changes]',
'value' => $preferences->notifyOnOwnPageChanges(),
'label' => trans('preferences.notifications_opt_own_page_changes'),
])
</div>
@if (!setting('app-disable-comments'))
<div>
@include('form.toggle-switch', [
'name' => 'preferences[own-page-comments]',
'value' => $preferences->notifyOnOwnPageComments(),
'label' => trans('preferences.notifications_opt_own_page_comments'),
])
</div>
<div>
@include('form.toggle-switch', [
'name' => 'preferences[comment-replies]',
'value' => $preferences->notifyOnCommentReplies(),
'label' => trans('preferences.notifications_opt_comment_replies'),
])
</div>
@endif
</div>
<div class="mt-auto">
<button class="button">{{ trans('preferences.notifications_save') }}</button>
</div>
</div>
</form>
</section>
<section class="card content-wrap auto-height">
<h2 class="list-heading">{{ trans('preferences.notifications_watched') }}</h2>
<p class="text-small text-muted">{{ trans('preferences.notifications_watched_desc') }}</p>
@if($watches->isEmpty())
<p class="text-muted italic">{{ trans('common.no_items') }}</p>
@else
<div class="item-list">
@foreach($watches as $watch)
<div class="flex-container-row justify-space-between item-list-row items-center wrap px-m py-s">
<div class="py-xs px-s min-width-m">
@include('entities.icon-link', ['entity' => $watch->watchable])
</div>
<div class="py-xs min-width-m text-m-right px-m">
@icon('watch' . ($watch->ignoring() ? '-ignore' : ''))
{{ trans('entities.watch_title_' . $watch->getLevelName()) }}
</div>
</div>
@endforeach
</div>
@endif
<div class="my-m">{{ $watches->links() }}</div>
</section>
@stop

View File

@ -0,0 +1,71 @@
@extends('users.account.layout')
@section('main')
<section class="card content-wrap">
<form action="{{ url('/my-account/shortcuts') }}" method="post">
{{ method_field('put') }}
{{ csrf_field() }}
<h1 class="list-heading">{{ trans('preferences.shortcuts_interface') }}</h1>
<div class="flex-container-row items-center gap-m wrap mb-m">
<p class="flex mb-none min-width-m text-small text-muted">
{{ trans('preferences.shortcuts_toggle_desc') }}
{{ trans('preferences.shortcuts_customize_desc') }}
</p>
<div class="flex min-width-m text-m-center">
@include('form.toggle-switch', [
'name' => 'enabled',
'value' => $enabled,
'label' => trans('preferences.shortcuts_toggle_label'),
])
</div>
</div>
<hr>
<h2 class="list-heading mb-m">{{ trans('preferences.shortcuts_section_navigation') }}</h2>
<div class="flex-container-row wrap gap-m mb-xl">
<div class="flex min-width-l item-list">
@include('users.account.parts.shortcut-control', ['label' => trans('common.homepage'), 'id' => 'home_view'])
@include('users.account.parts.shortcut-control', ['label' => trans('entities.shelves'), 'id' => 'shelves_view'])
@include('users.account.parts.shortcut-control', ['label' => trans('entities.books'), 'id' => 'books_view'])
@include('users.account.parts.shortcut-control', ['label' => trans('settings.settings'), 'id' => 'settings_view'])
@include('users.account.parts.shortcut-control', ['label' => trans('entities.my_favourites'), 'id' => 'favourites_view'])
</div>
<div class="flex min-width-l item-list">
@include('users.account.parts.shortcut-control', ['label' => trans('common.view_profile'), 'id' => 'profile_view'])
@include('users.account.parts.shortcut-control', ['label' => trans('auth.logout'), 'id' => 'logout'])
@include('users.account.parts.shortcut-control', ['label' => trans('common.global_search'), 'id' => 'global_search'])
@include('users.account.parts.shortcut-control', ['label' => trans('common.next'), 'id' => 'next'])
@include('users.account.parts.shortcut-control', ['label' => trans('common.previous'), 'id' => 'previous'])
</div>
</div>
<h2 class="list-heading mb-m">{{ trans('preferences.shortcuts_section_actions') }}</h2>
<div class="flex-container-row wrap gap-m mb-xl">
<div class="flex min-width-l item-list">
@include('users.account.parts.shortcut-control', ['label' => trans('common.new'), 'id' => 'new'])
@include('users.account.parts.shortcut-control', ['label' => trans('common.edit'), 'id' => 'edit'])
@include('users.account.parts.shortcut-control', ['label' => trans('common.copy'), 'id' => 'copy'])
@include('users.account.parts.shortcut-control', ['label' => trans('common.delete'), 'id' => 'delete'])
@include('users.account.parts.shortcut-control', ['label' => trans('common.favourite'), 'id' => 'favourite'])
</div>
<div class="flex min-width-l item-list">
@include('users.account.parts.shortcut-control', ['label' => trans('entities.export'), 'id' => 'export'])
@include('users.account.parts.shortcut-control', ['label' => trans('common.sort'), 'id' => 'sort'])
@include('users.account.parts.shortcut-control', ['label' => trans('entities.permissions'), 'id' => 'permissions'])
@include('users.account.parts.shortcut-control', ['label' => trans('common.move'), 'id' => 'move'])
@include('users.account.parts.shortcut-control', ['label' => trans('entities.revisions'), 'id' => 'revisions'])
</div>
</div>
<p class="text-small text-muted">{{ trans('preferences.shortcuts_overlay_desc') }}</p>
<div class="form-group text-right">
<button class="button">{{ trans('preferences.shortcuts_save') }}</button>
</div>
</form>
</section>
@stop

View File

@ -1,75 +0,0 @@
@extends('layouts.simple')
@section('body')
<div class="container small my-xl">
<section class="card content-wrap auto-height">
<form action="{{ url('/preferences/notifications') }}" method="post">
{{ method_field('put') }}
{{ csrf_field() }}
<h1 class="list-heading">{{ trans('preferences.notifications') }}</h1>
<p class="text-small text-muted">{{ trans('preferences.notifications_desc') }}</p>
<div class="flex-container-row wrap justify-space-between pb-m">
<div class="toggle-switch-list min-width-l">
<div>
@include('form.toggle-switch', [
'name' => 'preferences[own-page-changes]',
'value' => $preferences->notifyOnOwnPageChanges(),
'label' => trans('preferences.notifications_opt_own_page_changes'),
])
</div>
@if (!setting('app-disable-comments'))
<div>
@include('form.toggle-switch', [
'name' => 'preferences[own-page-comments]',
'value' => $preferences->notifyOnOwnPageComments(),
'label' => trans('preferences.notifications_opt_own_page_comments'),
])
</div>
<div>
@include('form.toggle-switch', [
'name' => 'preferences[comment-replies]',
'value' => $preferences->notifyOnCommentReplies(),
'label' => trans('preferences.notifications_opt_comment_replies'),
])
</div>
@endif
</div>
<div class="mt-auto">
<button class="button">{{ trans('preferences.notifications_save') }}</button>
</div>
</div>
</form>
</section>
<section class="card content-wrap auto-height">
<h2 class="list-heading">{{ trans('preferences.notifications_watched') }}</h2>
<p class="text-small text-muted">{{ trans('preferences.notifications_watched_desc') }}</p>
@if($watches->isEmpty())
<p class="text-muted italic">{{ trans('common.no_items') }}</p>
@else
<div class="item-list">
@foreach($watches as $watch)
<div class="flex-container-row justify-space-between item-list-row items-center wrap px-m py-s">
<div class="py-xs px-s min-width-m">
@include('entities.icon-link', ['entity' => $watch->watchable])
</div>
<div class="py-xs min-width-m text-m-right px-m">
@icon('watch' . ($watch->ignoring() ? '-ignore' : ''))
{{ trans('entities.watch_title_' . $watch->getLevelName()) }}
</div>
</div>
@endforeach
</div>
@endif
<div class="my-m">{{ $watches->links() }}</div>
</section>
</div>
@stop

View File

@ -1,75 +0,0 @@
@extends('layouts.simple')
@section('body')
<div class="container small my-xl">
<section class="card content-wrap">
<form action="{{ url('/preferences/shortcuts') }}" method="post">
{{ method_field('put') }}
{{ csrf_field() }}
<h1 class="list-heading">{{ trans('preferences.shortcuts_interface') }}</h1>
<div class="flex-container-row items-center gap-m wrap mb-m">
<p class="flex mb-none min-width-m text-small text-muted">
{{ trans('preferences.shortcuts_toggle_desc') }}
{{ trans('preferences.shortcuts_customize_desc') }}
</p>
<div class="flex min-width-m text-m-center">
@include('form.toggle-switch', [
'name' => 'enabled',
'value' => $enabled,
'label' => trans('preferences.shortcuts_toggle_label'),
])
</div>
</div>
<hr>
<h2 class="list-heading mb-m">{{ trans('preferences.shortcuts_section_navigation') }}</h2>
<div class="flex-container-row wrap gap-m mb-xl">
<div class="flex min-width-l item-list">
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.homepage'), 'id' => 'home_view'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('entities.shelves'), 'id' => 'shelves_view'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('entities.books'), 'id' => 'books_view'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('settings.settings'), 'id' => 'settings_view'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('entities.my_favourites'), 'id' => 'favourites_view'])
</div>
<div class="flex min-width-l item-list">
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.view_profile'), 'id' => 'profile_view'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('auth.logout'), 'id' => 'logout'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.global_search'), 'id' => 'global_search'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.next'), 'id' => 'next'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.previous'), 'id' => 'previous'])
</div>
</div>
<h2 class="list-heading mb-m">{{ trans('preferences.shortcuts_section_actions') }}</h2>
<div class="flex-container-row wrap gap-m mb-xl">
<div class="flex min-width-l item-list">
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.new'), 'id' => 'new'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.edit'), 'id' => 'edit'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.copy'), 'id' => 'copy'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.delete'), 'id' => 'delete'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.favourite'), 'id' => 'favourite'])
</div>
<div class="flex min-width-l item-list">
@include('users.preferences.parts.shortcut-control', ['label' => trans('entities.export'), 'id' => 'export'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.sort'), 'id' => 'sort'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('entities.permissions'), 'id' => 'permissions'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.move'), 'id' => 'move'])
@include('users.preferences.parts.shortcut-control', ['label' => trans('entities.revisions'), 'id' => 'revisions'])
</div>
</div>
<p class="text-small text-muted">{{ trans('preferences.shortcuts_overlay_desc') }}</p>
<div class="form-group text-right">
<button class="button">{{ trans('preferences.shortcuts_save') }}</button>
</div>
</form>
</section>
</div>
@stop

View File

@ -232,18 +232,17 @@ Route::middleware('auth')->group(function () {
Route::put('/settings/users/{id}', [UserControllers\UserController::class, 'update']);
Route::delete('/settings/users/{id}', [UserControllers\UserController::class, 'destroy']);
// User Preferences
Route::get('/preferences', [UserControllers\UserPreferencesController::class, 'index']);
Route::get('/preferences/shortcuts', [UserControllers\UserPreferencesController::class, 'showShortcuts']);
Route::put('/preferences/shortcuts', [UserControllers\UserPreferencesController::class, 'updateShortcuts']);
Route::get('/preferences/notifications', [UserControllers\UserPreferencesController::class, 'showNotifications']);
Route::put('/preferences/notifications', [UserControllers\UserPreferencesController::class, 'updateNotifications']);
// User Account
Route::get('/my-account', [UserControllers\UserAccountController::class, 'index']);
Route::get('/my-account/shortcuts', [UserControllers\UserAccountController::class, 'showShortcuts']);
Route::put('/my-account/shortcuts', [UserControllers\UserAccountController::class, 'updateShortcuts']);
Route::get('/my-account/notifications', [UserControllers\UserAccountController::class, 'showNotifications']);
Route::put('/my-account/notifications', [UserControllers\UserAccountController::class, 'updateNotifications']);
Route::patch('/preferences/change-view/{type}', [UserControllers\UserPreferencesController::class, 'changeView']);
Route::patch('/preferences/change-sort/{type}', [UserControllers\UserPreferencesController::class, 'changeSort']);
Route::patch('/preferences/change-expansion/{type}', [UserControllers\UserPreferencesController::class, 'changeExpansion']);
Route::patch('/preferences/toggle-dark-mode', [UserControllers\UserPreferencesController::class, 'toggleDarkMode']);
Route::patch('/preferences/update-code-language-favourite', [UserControllers\UserPreferencesController::class, 'updateCodeLanguageFavourite']);
Route::patch('/preferences/update-boolean', [UserControllers\UserPreferencesController::class, 'updateBooleanPreference']);
// User API Tokens
Route::get('/settings/users/{userId}/create-api-token', [UserApiTokenController::class, 'create']);