mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-17 01:57:50 +00:00
Merge pull request #4615 from BookStackApp/user_account
User preferences/options cleanup
This commit is contained in:
commit
995b7d61e9
41 changed files with 1258 additions and 619 deletions
app
Access
Api
Users/Controllers
lang/en
resources
icons
views
entities
layouts/parts
settings
users
account
auth.blade.phpdelete.blade.phplayout.blade.phpnotifications.blade.php
parts
profile.blade.phpshortcuts.blade.phpapi-tokens
delete.blade.phpedit.blade.phpparts
preferences
routes
tests
|
@ -16,22 +16,12 @@ use Laravel\Socialite\Contracts\User as SocialUser;
|
|||
|
||||
class SocialController extends Controller
|
||||
{
|
||||
protected SocialAuthService $socialAuthService;
|
||||
protected RegistrationService $registrationService;
|
||||
protected LoginService $loginService;
|
||||
|
||||
/**
|
||||
* SocialController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
SocialAuthService $socialAuthService,
|
||||
RegistrationService $registrationService,
|
||||
LoginService $loginService
|
||||
protected SocialAuthService $socialAuthService,
|
||||
protected RegistrationService $registrationService,
|
||||
protected LoginService $loginService,
|
||||
) {
|
||||
$this->middleware('guest')->only(['register']);
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->registrationService = $registrationService;
|
||||
$this->loginService = $loginService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -112,7 +102,7 @@ class SocialController extends Controller
|
|||
$this->socialAuthService->detachSocialAccount($socialDriver);
|
||||
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)]));
|
||||
|
||||
return redirect(user()->getEditUrl());
|
||||
return redirect('/my-account/auth#social-accounts');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -154,21 +154,21 @@ class SocialAuthService
|
|||
$currentUser->socialAccounts()->save($account);
|
||||
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
|
||||
|
||||
return redirect($currentUser->getEditUrl());
|
||||
return redirect('/my-account/auth#social_accounts');
|
||||
}
|
||||
|
||||
// When a user is logged in and the social account exists and is already linked to the current user.
|
||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
|
||||
session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver]));
|
||||
|
||||
return redirect($currentUser->getEditUrl());
|
||||
return redirect('/my-account/auth#social_accounts');
|
||||
}
|
||||
|
||||
// When a user is logged in, A social account exists but the users do not match.
|
||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
|
||||
session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver]));
|
||||
|
||||
return redirect($currentUser->getEditUrl());
|
||||
return redirect('/my-account/auth#social_accounts');
|
||||
}
|
||||
|
||||
// Otherwise let the user know this social account is not used by anyone.
|
||||
|
@ -214,6 +214,7 @@ class SocialAuthService
|
|||
|
||||
/**
|
||||
* Gets the names of the active social drivers.
|
||||
* @returns array<string, string>
|
||||
*/
|
||||
public function getActiveDrivers(): array
|
||||
{
|
||||
|
|
|
@ -31,6 +31,8 @@ class ApiDocsController extends ApiController
|
|||
|
||||
/**
|
||||
* Redirect to the API docs page.
|
||||
* Required as a controller method, instead of the Route::redirect helper,
|
||||
* to ensure the URL is generated correctly.
|
||||
*/
|
||||
public function redirect()
|
||||
{
|
||||
|
|
|
@ -52,4 +52,12 @@ class ApiToken extends Model implements Loggable
|
|||
{
|
||||
return "({$this->id}) {$this->name}; User: {$this->user->logDescriptor()}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for managing this token.
|
||||
*/
|
||||
public function getUrl(string $path = ''): string
|
||||
{
|
||||
return url("/api-tokens/{$this->user_id}/{$this->id}/" . trim($path, '/'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,16 +14,19 @@ class UserApiTokenController extends Controller
|
|||
/**
|
||||
* Show the form to create a new API token.
|
||||
*/
|
||||
public function create(int $userId)
|
||||
public function create(Request $request, int $userId)
|
||||
{
|
||||
// Ensure user is has access-api permission and is the current user or has permission to manage the current user.
|
||||
$this->checkPermission('access-api');
|
||||
$this->checkPermissionOrCurrentUser('users-manage', $userId);
|
||||
$this->updateContext($request);
|
||||
|
||||
$user = User::query()->findOrFail($userId);
|
||||
|
||||
$this->setPageTitle(trans('settings.user_api_token_create'));
|
||||
|
||||
return view('users.api-tokens.create', [
|
||||
'user' => $user,
|
||||
'back' => $this->getRedirectPath($user),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -60,22 +63,27 @@ class UserApiTokenController extends Controller
|
|||
session()->flash('api-token-secret:' . $token->id, $secret);
|
||||
$this->logActivity(ActivityType::API_TOKEN_CREATE, $token);
|
||||
|
||||
return redirect($user->getEditUrl('/api-tokens/' . $token->id));
|
||||
return redirect($token->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the details for a user API token, with access to edit.
|
||||
*/
|
||||
public function edit(int $userId, int $tokenId)
|
||||
public function edit(Request $request, int $userId, int $tokenId)
|
||||
{
|
||||
$this->updateContext($request);
|
||||
|
||||
[$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);
|
||||
$secret = session()->pull('api-token-secret:' . $token->id, null);
|
||||
|
||||
$this->setPageTitle(trans('settings.user_api_token'));
|
||||
|
||||
return view('users.api-tokens.edit', [
|
||||
'user' => $user,
|
||||
'token' => $token,
|
||||
'model' => $token,
|
||||
'secret' => $secret,
|
||||
'back' => $this->getRedirectPath($user),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -97,7 +105,7 @@ class UserApiTokenController extends Controller
|
|||
|
||||
$this->logActivity(ActivityType::API_TOKEN_UPDATE, $token);
|
||||
|
||||
return redirect($user->getEditUrl('/api-tokens/' . $token->id));
|
||||
return redirect($token->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -107,6 +115,8 @@ class UserApiTokenController extends Controller
|
|||
{
|
||||
[$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);
|
||||
|
||||
$this->setPageTitle(trans('settings.user_api_token_delete'));
|
||||
|
||||
return view('users.api-tokens.delete', [
|
||||
'user' => $user,
|
||||
'token' => $token,
|
||||
|
@ -123,7 +133,7 @@ class UserApiTokenController extends Controller
|
|||
|
||||
$this->logActivity(ActivityType::API_TOKEN_DELETE, $token);
|
||||
|
||||
return redirect($user->getEditUrl('#api_tokens'));
|
||||
return redirect($this->getRedirectPath($user));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -142,4 +152,30 @@ class UserApiTokenController extends Controller
|
|||
|
||||
return [$user, $token];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the context for where the user is coming from to manage API tokens.
|
||||
* (Track of location for correct return redirects)
|
||||
*/
|
||||
protected function updateContext(Request $request): void
|
||||
{
|
||||
$context = $request->query('context');
|
||||
if ($context) {
|
||||
session()->put('api-token-context', $context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the redirect path for the current api token editing session.
|
||||
* Attempts to recall the context of where the user is editing from.
|
||||
*/
|
||||
protected function getRedirectPath(User $relatedUser): string
|
||||
{
|
||||
$context = session()->get('api-token-context');
|
||||
if ($context === 'settings' || user()->id !== $relatedUser->id) {
|
||||
return $relatedUser->getEditUrl('#api_tokens');
|
||||
}
|
||||
|
||||
return url('/my-account/auth#api_tokens');
|
||||
}
|
||||
}
|
||||
|
|
223
app/Users/Controllers/UserAccountController.php
Normal file
223
app/Users/Controllers/UserAccountController.php
Normal file
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
|
||||
namespace BookStack\Users\Controllers;
|
||||
|
||||
use BookStack\Access\SocialAuthService;
|
||||
use BookStack\Http\Controller;
|
||||
use BookStack\Permissions\PermissionApplicator;
|
||||
use BookStack\Settings\UserNotificationPreferences;
|
||||
use BookStack\Settings\UserShortcutMap;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use BookStack\Users\UserRepo;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class UserAccountController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected UserRepo $userRepo,
|
||||
) {
|
||||
$this->middleware(function (Request $request, Closure $next) {
|
||||
$this->preventGuestAccess();
|
||||
$this->preventAccessInDemoMode();
|
||||
return $next($request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect the root my-account path to the main/first category.
|
||||
* Required as a controller method, instead of the Route::redirect helper,
|
||||
* to ensure the URL is generated correctly.
|
||||
*/
|
||||
public function redirect()
|
||||
{
|
||||
return redirect('/my-account/profile');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the profile form interface.
|
||||
*/
|
||||
public function showProfile()
|
||||
{
|
||||
$this->setPageTitle(trans('preferences.profile'));
|
||||
|
||||
return view('users.account.profile', [
|
||||
'model' => user(),
|
||||
'category' => 'profile',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the submission of the user profile form.
|
||||
*/
|
||||
public function updateProfile(Request $request, ImageRepo $imageRepo)
|
||||
{
|
||||
$user = user();
|
||||
$validated = $this->validate($request, [
|
||||
'name' => ['min:2', 'max:100'],
|
||||
'email' => ['min:2', 'email', 'unique:users,email,' . $user->id],
|
||||
'language' => ['string', 'max:15', 'alpha_dash'],
|
||||
'profile_image' => array_merge(['nullable'], $this->getImageValidationRules()),
|
||||
]);
|
||||
|
||||
$this->userRepo->update($user, $validated, userCan('users-manage'));
|
||||
|
||||
// Save profile image if in request
|
||||
if ($request->hasFile('profile_image')) {
|
||||
$imageUpload = $request->file('profile_image');
|
||||
$imageRepo->destroyImage($user->avatar);
|
||||
$image = $imageRepo->saveNew($imageUpload, 'user', $user->id);
|
||||
$user->image_id = $image->id;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
// Delete the profile image if reset option is in request
|
||||
if ($request->has('profile_image_reset')) {
|
||||
$imageRepo->destroyImage($user->avatar);
|
||||
$user->image_id = 0;
|
||||
$user->save();
|
||||
}
|
||||
|
||||
return redirect('/my-account/profile');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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', [
|
||||
'category' => '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');
|
||||
|
||||
$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', [
|
||||
'category' => 'notifications',
|
||||
'preferences' => $preferences,
|
||||
'watches' => $watches,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the notification preferences for the current user.
|
||||
*/
|
||||
public function updateNotifications(Request $request)
|
||||
{
|
||||
$this->checkPermission('receive-notifications');
|
||||
$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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the view for the "Access & Security" account options.
|
||||
*/
|
||||
public function showAuth(SocialAuthService $socialAuthService)
|
||||
{
|
||||
$mfaMethods = user()->mfaValues()->get()->groupBy('method');
|
||||
|
||||
$this->setPageTitle(trans('preferences.auth'));
|
||||
|
||||
return view('users.account.auth', [
|
||||
'category' => 'auth',
|
||||
'mfaMethods' => $mfaMethods,
|
||||
'authMethod' => config('auth.method'),
|
||||
'activeSocialDrivers' => $socialAuthService->getActiveDrivers(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the submission for the auth change password form.
|
||||
*/
|
||||
public function updatePassword(Request $request)
|
||||
{
|
||||
if (config('auth.method') !== 'standard') {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
|
||||
$validated = $this->validate($request, [
|
||||
'password' => ['required_with:password_confirm', Password::default()],
|
||||
'password-confirm' => ['same:password', 'required_with:password'],
|
||||
]);
|
||||
|
||||
$this->userRepo->update(user(), $validated, false);
|
||||
|
||||
$this->showSuccessNotification(trans('preferences.auth_change_password_success'));
|
||||
|
||||
return redirect('/my-account/auth');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the user self-delete page.
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
$this->setPageTitle(trans('preferences.delete_my_account'));
|
||||
|
||||
return view('users.account.delete', [
|
||||
'category' => 'profile',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the current user from the system.
|
||||
*/
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$this->preventAccessInDemoMode();
|
||||
|
||||
$requestNewOwnerId = intval($request->get('new_owner_id')) ?: null;
|
||||
$newOwnerId = userCan('users-manage') ? $requestNewOwnerId : null;
|
||||
|
||||
$this->userRepo->destroy(user(), $newOwnerId);
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
|
@ -103,8 +103,7 @@ class UserController extends Controller
|
|||
*/
|
||||
public function edit(int $id, SocialAuthService $socialAuthService)
|
||||
{
|
||||
$this->preventGuestAccess();
|
||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||
$this->checkPermission('users-manage');
|
||||
|
||||
$user = $this->userRepo->getById($id);
|
||||
$user->load(['apiTokens', 'mfaValues']);
|
||||
|
@ -134,8 +133,7 @@ class UserController extends Controller
|
|||
public function update(Request $request, int $id)
|
||||
{
|
||||
$this->preventAccessInDemoMode();
|
||||
$this->preventGuestAccess();
|
||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||
$this->checkPermission('users-manage');
|
||||
|
||||
$validated = $this->validate($request, [
|
||||
'name' => ['min:2', 'max:100'],
|
||||
|
@ -150,7 +148,7 @@ class UserController extends Controller
|
|||
]);
|
||||
|
||||
$user = $this->userRepo->getById($id);
|
||||
$this->userRepo->update($user, $validated, userCan('users-manage'));
|
||||
$this->userRepo->update($user, $validated, true);
|
||||
|
||||
// Save profile image if in request
|
||||
if ($request->hasFile('profile_image')) {
|
||||
|
@ -168,9 +166,7 @@ class UserController extends Controller
|
|||
$user->save();
|
||||
}
|
||||
|
||||
$redirectUrl = userCan('users-manage') ? '/settings/users' : "/settings/users/{$user->id}";
|
||||
|
||||
return redirect($redirectUrl);
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -178,8 +174,7 @@ class UserController extends Controller
|
|||
*/
|
||||
public function delete(int $id)
|
||||
{
|
||||
$this->preventGuestAccess();
|
||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||
$this->checkPermission('users-manage');
|
||||
|
||||
$user = $this->userRepo->getById($id);
|
||||
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
|
||||
|
@ -195,8 +190,7 @@ class UserController extends Controller
|
|||
public function destroy(Request $request, int $id)
|
||||
{
|
||||
$this->preventAccessInDemoMode();
|
||||
$this->preventGuestAccess();
|
||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
||||
$this->checkPermission('users-manage');
|
||||
|
||||
$user = $this->userRepo->getById($id);
|
||||
$newOwnerId = intval($request->get('new_owner_id')) ?: null;
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -52,6 +52,7 @@ return [
|
|||
'filter_clear' => 'Clear Filter',
|
||||
'download' => 'Download',
|
||||
'open_in_tab' => 'Open in Tab',
|
||||
'open' => 'Open',
|
||||
|
||||
// Sort Options
|
||||
'sort_options' => 'Sort Options',
|
||||
|
|
|
@ -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',
|
||||
|
@ -29,5 +29,23 @@ return [
|
|||
'notifications_watched' => 'Watched & Ignored Items',
|
||||
'notifications_watched_desc' => ' Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.',
|
||||
|
||||
'profile_overview_desc' => ' Manage your user profile details including preferred language and authentication options.',
|
||||
'auth' => 'Access & Security',
|
||||
'auth_change_password' => 'Change Password',
|
||||
'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.',
|
||||
'auth_change_password_success' => 'Password has been updated!',
|
||||
|
||||
'profile' => 'Profile Details',
|
||||
'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.',
|
||||
'profile_view_public' => 'View Public Profile',
|
||||
'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.',
|
||||
'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.',
|
||||
'profile_email_no_permission' => 'Unfortunately you don\'t have permission to change your email address. If you want to change this, you\'d need to ask an administrator to change this for you.',
|
||||
'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.',
|
||||
'profile_admin_options' => 'Administrator Options',
|
||||
'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the "Settings > Users" area of the application.',
|
||||
|
||||
'delete_account' => 'Delete Account',
|
||||
'delete_my_account' => 'Delete My Account',
|
||||
'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\'ve created, such as created pages and uploaded images, will remain.',
|
||||
'delete_my_account_warning' => 'Are you sure you want to delete your account?',
|
||||
];
|
||||
|
|
|
@ -193,8 +193,8 @@ return [
|
|||
'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',
|
||||
'users_send_invite_option' => 'Send user invite email',
|
||||
'users_external_auth_id' => 'External Authentication ID',
|
||||
'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your external authentication system.',
|
||||
'users_password_warning' => 'Only fill the below if you would like to change your password.',
|
||||
'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.',
|
||||
'users_password_warning' => 'Only fill the below if you would like to change the password for this user.',
|
||||
'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',
|
||||
'users_delete' => 'Delete User',
|
||||
'users_delete_named' => 'Delete user :userName',
|
||||
|
@ -210,12 +210,16 @@ return [
|
|||
'users_preferred_language' => 'Preferred Language',
|
||||
'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',
|
||||
'users_social_accounts' => 'Social Accounts',
|
||||
'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.',
|
||||
'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',
|
||||
'users_social_connect' => 'Connect Account',
|
||||
'users_social_disconnect' => 'Disconnect Account',
|
||||
'users_social_status_connected' => 'Connected',
|
||||
'users_social_status_disconnected' => 'Disconnected',
|
||||
'users_social_connected' => ':socialAccount account was successfully attached to your profile.',
|
||||
'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',
|
||||
'users_api_tokens' => 'API Tokens',
|
||||
'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.',
|
||||
'users_api_tokens_none' => 'No API tokens have been created for this user',
|
||||
'users_api_tokens_create' => 'Create Token',
|
||||
'users_api_tokens_expires' => 'Expires',
|
||||
|
|
1
resources/icons/notifications.svg
Normal file
1
resources/icons/notifications.svg
Normal 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 (image error) Size: 238 B |
1
resources/icons/security.svg
Normal file
1
resources/icons/security.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
|
After (image error) Size: 303 B |
|
@ -37,7 +37,7 @@
|
|||
</li>
|
||||
@endforeach
|
||||
<li>
|
||||
<a href="{{ url('/preferences/notifications') }}"
|
||||
<a href="{{ url('/my-account/notifications') }}"
|
||||
target="_blank"
|
||||
class="text-item text-muted text-small break-text">{{ trans('entities.watch_change_default') }}</a>
|
||||
</li>
|
||||
|
|
|
@ -18,11 +18,16 @@
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ $user->getEditUrl() }}" class="icon-item">
|
||||
@icon('edit')
|
||||
<div>{{ trans('common.edit_profile') }}</div>
|
||||
<a href="{{ url('/my-account') }}" class="icon-item">
|
||||
@icon('user-preferences')
|
||||
<div>{{ trans('preferences.my_account') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
<li><hr></li>
|
||||
<li>
|
||||
@include('common.dark-mode-toggle', ['classes' => 'icon-item'])
|
||||
</li>
|
||||
<li><hr></li>
|
||||
<li>
|
||||
<form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}"
|
||||
method="post">
|
||||
|
@ -33,15 +38,5 @@
|
|||
</button>
|
||||
</form>
|
||||
</li>
|
||||
<li><hr></li>
|
||||
<li>
|
||||
<a href="{{ url('/preferences') }}" class="icon-item">
|
||||
@icon('user-preferences')
|
||||
<div>{{ trans('preferences.preferences') }}</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@include('common.dark-mode-toggle', ['classes' => 'icon-item'])
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
|
@ -12,7 +12,7 @@
|
|||
<nav class="active-link-list in-sidebar">
|
||||
<a href="{{ url('/settings/features') }}" class="{{ $category === 'features' ? 'active' : '' }}">@icon('star') {{ trans('settings.app_features_security') }}</a>
|
||||
<a href="{{ url('/settings/customization') }}" class="{{ $category === 'customization' ? 'active' : '' }}">@icon('palette') {{ trans('settings.app_customization') }}</a>
|
||||
<a href="{{ url('/settings/registration') }}" class="{{ $category === 'registration' ? 'active' : '' }}">@icon('lock') {{ trans('settings.reg_settings') }}</a>
|
||||
<a href="{{ url('/settings/registration') }}" class="{{ $category === 'registration' ? 'active' : '' }}">@icon('security') {{ trans('settings.reg_settings') }}</a>
|
||||
</nav>
|
||||
|
||||
<h5 class="mt-xl">{{ trans('settings.system_version') }}</h5>
|
||||
|
|
87
resources/views/users/account/auth.blade.php
Normal file
87
resources/views/users/account/auth.blade.php
Normal file
|
@ -0,0 +1,87 @@
|
|||
@extends('users.account.layout')
|
||||
|
||||
@section('main')
|
||||
|
||||
@if($authMethod === 'standard')
|
||||
<section class="card content-wrap auto-height">
|
||||
<form action="{{ url('/my-account/auth/password') }}" method="post">
|
||||
{{ method_field('put') }}
|
||||
{{ csrf_field() }}
|
||||
|
||||
<h2 class="list-heading">{{ trans('preferences.auth_change_password') }}</h2>
|
||||
|
||||
<p class="text-muted text-small">
|
||||
{{ trans('preferences.auth_change_password_desc') }}
|
||||
</p>
|
||||
|
||||
<div class="grid half mt-m gap-xl wrap stretch-inputs mb-m">
|
||||
<div>
|
||||
<label for="password">{{ trans('auth.password') }}</label>
|
||||
@include('form.password', ['name' => 'password', 'autocomplete' => 'new-password'])
|
||||
</div>
|
||||
<div>
|
||||
<label for="password-confirm">{{ trans('auth.password_confirm') }}</label>
|
||||
@include('form.password', ['name' => 'password-confirm'])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group text-right">
|
||||
<button class="button">{{ trans('common.update') }}</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<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 text-small">{{ 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>
|
||||
|
||||
@if(count($activeSocialDrivers) > 0)
|
||||
<section id="social-accounts" class="card content-wrap auto-height">
|
||||
<h2 class="list-heading">{{ trans('settings.users_social_accounts') }}</h2>
|
||||
<p class="text-muted text-small">{{ trans('settings.users_social_accounts_info') }}</p>
|
||||
<div class="container">
|
||||
<div class="grid third">
|
||||
@foreach($activeSocialDrivers as $driver => $enabled)
|
||||
<div class="text-center mb-m">
|
||||
<div role="presentation">@icon('auth/'. $driver, ['style' => 'width: 56px;height: 56px;'])</div>
|
||||
<div>
|
||||
@if(user()->hasSocialAccount($driver))
|
||||
<form action="{{ url("/login/service/{$driver}/detach") }}" method="POST">
|
||||
{{ csrf_field() }}
|
||||
<button aria-label="{{ trans('settings.users_social_disconnect') }} - {{ $driver }}"
|
||||
class="button small outline">{{ trans('settings.users_social_disconnect') }}</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ url("/login/service/{$driver}") }}"
|
||||
aria-label="{{ trans('settings.users_social_connect') }} - {{ $driver }}"
|
||||
class="button small outline">{{ trans('settings.users_social_connect') }}</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@if(userCan('access-api'))
|
||||
@include('users.api-tokens.parts.list', ['user' => user(), 'context' => 'my-account'])
|
||||
@endif
|
||||
@stop
|
43
resources/views/users/account/delete.blade.php
Normal file
43
resources/views/users/account/delete.blade.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
@extends('users.account.layout')
|
||||
|
||||
@section('main')
|
||||
|
||||
<div class="card content-wrap auto-height">
|
||||
<form action="{{ url("/my-account") }}" method="POST">
|
||||
{{ csrf_field() }}
|
||||
{{ method_field('delete') }}
|
||||
|
||||
|
||||
<h1 class="list-heading">{{ trans('preferences.delete_my_account') }}</h1>
|
||||
|
||||
<p>{{ trans('preferences.delete_my_account_desc') }}</p>
|
||||
|
||||
@if(userCan('users-manage'))
|
||||
<hr class="my-l">
|
||||
|
||||
<div class="grid half gap-xl v-center">
|
||||
<div>
|
||||
<label class="setting-list-label">{{ trans('settings.users_migrate_ownership') }}</label>
|
||||
<p class="small">{{ trans('settings.users_migrate_ownership_desc') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
@include('form.user-select', ['name' => 'new_owner_id', 'user' => null])
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<hr class="my-l">
|
||||
|
||||
<div class="grid half">
|
||||
<p class="text-neg"><strong>{{ trans('preferences.delete_my_account_warning') }}</strong></p>
|
||||
<div class="text-right">
|
||||
<a href="{{ url("/my-account/profile") }}"
|
||||
class="button outline">{{ trans('common.cancel') }}</a>
|
||||
<button type="submit" class="button">{{ trans('common.confirm') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@stop
|
29
resources/views/users/account/layout.blade.php
Normal file
29
resources/views/users/account/layout.blade.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
@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/profile') }}" class="{{ $category === 'profile' ? 'active' : '' }}">@icon('user') {{ trans('preferences.profile') }}</a>
|
||||
<a href="{{ url('/my-account/auth') }}" class="{{ $category === 'auth' ? 'active' : '' }}">@icon('security') {{ trans('preferences.auth') }}</a>
|
||||
<a href="{{ url('/my-account/shortcuts') }}" class="{{ $category === 'shortcuts' ? 'active' : '' }}">@icon('shortcuts') {{ trans('preferences.shortcuts_interface') }}</a>
|
||||
@if(userCan('receive-notifications'))
|
||||
<a href="{{ url('/my-account/notifications') }}" class="{{ $category === 'notifications' ? 'active' : '' }}">@icon('notifications') {{ trans('preferences.notifications') }}</a>
|
||||
@endif
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@yield('main')
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@stop
|
71
resources/views/users/account/notifications.blade.php
Normal file
71
resources/views/users/account/notifications.blade.php
Normal 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
|
91
resources/views/users/account/profile.blade.php
Normal file
91
resources/views/users/account/profile.blade.php
Normal file
|
@ -0,0 +1,91 @@
|
|||
@extends('users.account.layout')
|
||||
|
||||
@section('main')
|
||||
|
||||
<section class="card content-wrap auto-height">
|
||||
<form action="{{ url('/my-account/profile') }}" method="post" enctype="multipart/form-data">
|
||||
{{ method_field('put') }}
|
||||
{{ csrf_field() }}
|
||||
|
||||
<div class="flex-container-row gap-l items-center wrap justify-space-between">
|
||||
<h1 class="list-heading">{{ trans('preferences.profile') }}</h1>
|
||||
<div>
|
||||
<a href="{{ user()->getProfileUrl() }}" class="button outline">{{ trans('preferences.profile_view_public') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-muted text-small mb-none">{{ trans('preferences.profile_desc') }}</p>
|
||||
|
||||
<div class="setting-list">
|
||||
|
||||
<div class="flex-container-row gap-l items-center wrap">
|
||||
<div class="flex">
|
||||
<label class="setting-list-label" for="name">{{ trans('auth.name') }}</label>
|
||||
<p class="text-small mb-none">{{ trans('preferences.profile_name_desc') }}</p>
|
||||
</div>
|
||||
<div class="flex stretch-inputs">
|
||||
@include('form.text', ['name' => 'name'])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex-container-row gap-l items-center wrap">
|
||||
<div class="flex">
|
||||
<label class="setting-list-label" for="email">{{ trans('auth.email') }}</label>
|
||||
<p class="text-small mb-none">{{ trans('preferences.profile_email_desc') }}</p>
|
||||
</div>
|
||||
<div class="flex stretch-inputs">
|
||||
@include('form.text', ['name' => 'email', 'disabled' => !userCan('users-manage')])
|
||||
</div>
|
||||
</div>
|
||||
@if(!userCan('users-manage'))
|
||||
<p class="text-small text-muted">{{ trans('preferences.profile_email_no_permission') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="grid half gap-xl">
|
||||
<div>
|
||||
<label for="user-avatar"
|
||||
class="setting-list-label">{{ trans('settings.users_avatar') }}</label>
|
||||
<p class="text-small">{{ trans('preferences.profile_avatar_desc') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
@include('form.image-picker', [
|
||||
'resizeHeight' => '512',
|
||||
'resizeWidth' => '512',
|
||||
'showRemove' => false,
|
||||
'defaultImage' => url('/user_avatar.png'),
|
||||
'currentImage' => user()->getAvatar(80),
|
||||
'currentId' => user()->image_id,
|
||||
'name' => 'profile_image',
|
||||
'imageClass' => 'avatar large'
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@include('users.parts.language-option-row', ['value' => old('language') ?? user()->getLocale()->appLocale()])
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-group text-right">
|
||||
<a href="{{ url('/my-account/delete') }}" class="button outline">{{ trans('preferences.delete_account') }}</a>
|
||||
<button class="button">{{ trans('common.save') }}</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@if(userCan('users-manage'))
|
||||
<section class="card content-wrap auto-height">
|
||||
<div class="flex-container-row gap-l items-center wrap">
|
||||
<div class="flex">
|
||||
<h2 class="list-heading">{{ trans('preferences.profile_admin_options') }}</h2>
|
||||
<p class="text-small">{{ trans('preferences.profile_admin_options_desc') }}</p>
|
||||
</div>
|
||||
<div class="text-m-right">
|
||||
<a class="button outline" href="{{ user()->getEditUrl() }}">{{ trans('common.open') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
@stop
|
71
resources/views/users/account/shortcuts.blade.php
Normal file
71
resources/views/users/account/shortcuts.blade.php
Normal 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
|
|
@ -7,8 +7,8 @@
|
|||
<main class="card content-wrap auto-height">
|
||||
<h1 class="list-heading">{{ trans('settings.user_api_token_create') }}</h1>
|
||||
|
||||
<form action="{{ $user->getEditUrl('/create-api-token') }}" method="post">
|
||||
{!! csrf_field() !!}
|
||||
<form action="{{ url('/api-tokens/' . $user->id . '/create') }}" method="post">
|
||||
{{ csrf_field() }}
|
||||
|
||||
<div class="setting-list">
|
||||
@include('users.api-tokens.parts.form')
|
||||
|
@ -21,7 +21,7 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group text-right">
|
||||
<a href="{{ $user->getEditUrl('#api_tokens') }}" class="button outline">{{ trans('common.cancel') }}</a>
|
||||
<a href="{{ $back }}" class="button outline">{{ trans('common.cancel') }}</a>
|
||||
<button class="button" type="submit">{{ trans('common.save') }}</button>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -11,11 +11,11 @@
|
|||
<div class="grid half">
|
||||
<p class="text-neg"><strong>{{ trans('settings.user_api_token_delete_confirm') }}</strong></p>
|
||||
<div>
|
||||
<form action="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}" method="POST" class="text-right">
|
||||
{!! csrf_field() !!}
|
||||
{!! method_field('delete') !!}
|
||||
<form action="{{ $token->getUrl() }}" method="POST" class="text-right">
|
||||
{{ csrf_field() }}
|
||||
{{ method_field('delete') }}
|
||||
|
||||
<a href="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}" class="button outline">{{ trans('common.cancel') }}</a>
|
||||
<a href="{{ $token->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
|
||||
<button type="submit" class="button">{{ trans('common.confirm') }}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
<main class="card content-wrap auto-height">
|
||||
<h1 class="list-heading">{{ trans('settings.user_api_token') }}</h1>
|
||||
|
||||
<form action="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}" method="post">
|
||||
{!! method_field('put') !!}
|
||||
{!! csrf_field() !!}
|
||||
<form action="{{ $token->getUrl() }}" method="post">
|
||||
{{ method_field('put') }}
|
||||
{{ csrf_field() }}
|
||||
|
||||
<div class="setting-list">
|
||||
|
||||
|
@ -52,8 +52,8 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group text-right">
|
||||
<a href="{{ $user->getEditUrl('#api_tokens') }}" class="button outline">{{ trans('common.back') }}</a>
|
||||
<a href="{{ $user->getEditUrl('/api-tokens/' . $token->id . '/delete') }}" class="button outline">{{ trans('settings.user_api_token_delete') }}</a>
|
||||
<a href="{{ $back }}" class="button outline">{{ trans('common.back') }}</a>
|
||||
<a href="{{ $token->getUrl('/delete') }}" class="button outline">{{ trans('settings.user_api_token_delete') }}</a>
|
||||
<button class="button" type="submit">{{ trans('common.save') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,16 +4,17 @@
|
|||
<div class="text-right pt-xs">
|
||||
@if(userCan('access-api'))
|
||||
<a href="{{ url('/api/docs') }}" class="button outline">{{ trans('settings.users_api_tokens_docs') }}</a>
|
||||
<a href="{{ $user->getEditUrl('/create-api-token') }}" class="button outline">{{ trans('settings.users_api_tokens_create') }}</a>
|
||||
<a href="{{ url('/api-tokens/' . $user->id . '/create?context=' . $context) }}" class="button outline">{{ trans('settings.users_api_tokens_create') }}</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-small text-muted">{{ trans('settings.users_api_tokens_desc') }}</p>
|
||||
@if (count($user->apiTokens) > 0)
|
||||
<div class="item-list my-m">
|
||||
@foreach($user->apiTokens as $token)
|
||||
<div class="item-list-row flex-container-row items-center wrap py-xs gap-x-m">
|
||||
<div class="flex px-m py-xs min-width-m">
|
||||
<a href="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}">{{ $token->name }}</a> <br>
|
||||
<a href="{{ $token->getUrl("?context={$context}") }}">{{ $token->name }}</a> <br>
|
||||
<span class="small text-muted italic">{{ $token->token_id }}</span>
|
||||
</div>
|
||||
<div class="flex flex-container-row items-center min-width-m">
|
||||
|
@ -22,7 +23,7 @@
|
|||
{{ $token->expires_at->format('Y-m-d') ?? '' }}
|
||||
</div>
|
||||
<div class="flex px-m py-xs text-right">
|
||||
<a class="button outline small" href="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}">{{ trans('common.edit') }}</a>
|
||||
<a class="button outline small" href="{{ $token->getUrl("?context={$context}") }}">{{ trans('common.edit') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,33 +6,31 @@
|
|||
@include('settings.parts.navbar', ['selected' => 'users'])
|
||||
|
||||
<form action="{{ url("/settings/users/{$user->id}") }}" method="POST">
|
||||
{!! csrf_field() !!}
|
||||
{{ csrf_field() }}
|
||||
{{ method_field('delete') }}
|
||||
|
||||
<div class="card content-wrap auto-height">
|
||||
<h1 class="list-heading">{{ trans('settings.users_delete') }}</h1>
|
||||
|
||||
<p>{{ trans('settings.users_delete_warning', ['userName' => $user->name]) }}</p>
|
||||
|
||||
@if(userCan('users-manage'))
|
||||
<hr class="my-l">
|
||||
<hr class="my-l">
|
||||
|
||||
<div class="grid half gap-xl v-center">
|
||||
<div>
|
||||
<label class="setting-list-label">{{ trans('settings.users_migrate_ownership') }}</label>
|
||||
<p class="small">{{ trans('settings.users_migrate_ownership_desc') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
@include('form.user-select', ['name' => 'new_owner_id', 'user' => null])
|
||||
</div>
|
||||
<div class="grid half gap-xl v-center">
|
||||
<div>
|
||||
<label class="setting-list-label">{{ trans('settings.users_migrate_ownership') }}</label>
|
||||
<p class="small">{{ trans('settings.users_migrate_ownership_desc') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
<div>
|
||||
@include('form.user-select', ['name' => 'new_owner_id', 'user' => null])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-l">
|
||||
|
||||
<div class="grid half">
|
||||
<p class="text-neg"><strong>{{ trans('settings.users_delete_confirm') }}</strong></p>
|
||||
<div class="text-right">
|
||||
<input type="hidden" name="_method" value="DELETE">
|
||||
<a href="{{ url("/settings/users/{$user->id}") }}" class="button outline">{{ trans('common.cancel') }}</a>
|
||||
<button type="submit" class="button">{{ trans('common.confirm') }}</button>
|
||||
</div>
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<a href="{{ url(userCan('users-manage') ? "/settings/users" : "/") }}"
|
||||
<a href="{{ url("/settings/users") }}"
|
||||
class="button outline">{{ trans('common.cancel') }}</a>
|
||||
@if($authMethod !== 'system')
|
||||
<a href="{{ url("/settings/users/{$user->id}/delete") }}"
|
||||
|
@ -51,7 +51,7 @@
|
|||
|
||||
<section class="card content-wrap auto-height">
|
||||
<h2 class="list-heading">{{ trans('settings.users_mfa') }}</h2>
|
||||
<p>{{ trans('settings.users_mfa_desc') }}</p>
|
||||
<p class="text-small">{{ trans('settings.users_mfa_desc') }}</p>
|
||||
<div class="grid half gap-xl v-center pb-s">
|
||||
<div>
|
||||
@if ($mfaMethods->count() > 0)
|
||||
|
@ -71,28 +71,28 @@
|
|||
|
||||
</section>
|
||||
|
||||
@if(user()->id === $user->id && count($activeSocialDrivers) > 0)
|
||||
@if(count($activeSocialDrivers) > 0)
|
||||
<section class="card content-wrap auto-height">
|
||||
<h2 class="list-heading">{{ trans('settings.users_social_accounts') }}</h2>
|
||||
<p class="text-muted">{{ trans('settings.users_social_accounts_info') }}</p>
|
||||
<div class="flex-container-row items-center justify-space-between wrap">
|
||||
<h2 class="list-heading">{{ trans('settings.users_social_accounts') }}</h2>
|
||||
<div>
|
||||
@if(user()->id === $user->id)
|
||||
<a class="button outline" href="{{ url('/my-account/auth#social-accounts') }}">{{ trans('common.manage') }}</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted text-small">{{ trans('settings.users_social_accounts_desc') }}</p>
|
||||
<div class="container">
|
||||
<div class="grid third">
|
||||
@foreach($activeSocialDrivers as $driver => $enabled)
|
||||
@foreach($activeSocialDrivers as $driver => $driverName)
|
||||
<div class="text-center mb-m">
|
||||
<div role="presentation">@icon('auth/'. $driver, ['style' => 'width: 56px;height: 56px;'])</div>
|
||||
<div>
|
||||
@if($user->hasSocialAccount($driver))
|
||||
<form action="{{ url("/login/service/{$driver}/detach") }}" method="POST">
|
||||
{{ csrf_field() }}
|
||||
<button aria-label="{{ trans('settings.users_social_disconnect') }} - {{ $driver }}"
|
||||
class="button small outline">{{ trans('settings.users_social_disconnect') }}</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ url("/login/service/{$driver}") }}"
|
||||
aria-label="{{ trans('settings.users_social_connect') }} - {{ $driver }}"
|
||||
class="button small outline">{{ trans('settings.users_social_connect') }}</a>
|
||||
@endif
|
||||
</div>
|
||||
<p class="my-none bold">{{ $driverName }}</p>
|
||||
@if($user->hasSocialAccount($driver))
|
||||
<p class="text-pos bold text-small my-none">{{ trans('settings.users_social_status_connected') }}</p>
|
||||
@else
|
||||
<p class="text-neg bold text-small my-none">{{ trans('settings.users_social_status_disconnected') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
@ -100,9 +100,7 @@
|
|||
</section>
|
||||
@endif
|
||||
|
||||
@if((user()->id === $user->id && userCan('access-api')) || userCan('users-manage'))
|
||||
@include('users.api-tokens.parts.list', ['user' => $user])
|
||||
@endif
|
||||
@include('users.api-tokens.parts.list', ['user' => $user, 'context' => 'settings'])
|
||||
</div>
|
||||
|
||||
@stop
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
@if($authMethod === 'ldap' || $authMethod === 'system')
|
||||
<p class="small">{{ trans('settings.users_details_desc_no_email') }}</p>
|
||||
@endif
|
||||
<div class="grid half mt-m gap-xl">
|
||||
<div class="grid half mt-m gap-xl mb-l">
|
||||
<div>
|
||||
<label for="name">{{ trans('auth.name') }}</label>
|
||||
@include('form.text', ['name' => 'name'])
|
||||
|
@ -23,29 +23,26 @@
|
|||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-group collapsible mb-none" component="collapsible" id="external-auth-field">
|
||||
<button refs="collapsible@trigger" type="button" class="collapse-title text-link" aria-expanded="false">
|
||||
<label for="external-auth">{{ trans('settings.users_external_auth_id') }}</label>
|
||||
</button>
|
||||
<div refs="collapsible@content" class="collapse-content stretch-inputs">
|
||||
<p class="small">{{ trans('settings.users_external_auth_id_desc') }}</p>
|
||||
@include('form.text', ['name' => 'external_auth_id'])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(in_array($authMethod, ['ldap', 'saml2', 'oidc']) && userCan('users-manage'))
|
||||
<div class="grid half gap-xl v-center">
|
||||
<div>
|
||||
<label class="setting-list-label">{{ trans('settings.users_external_auth_id') }}</label>
|
||||
<p class="small">{{ trans('settings.users_external_auth_id_desc') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
@include('form.text', ['name' => 'external_auth_id'])
|
||||
</div>
|
||||
<div>
|
||||
<label for="role" class="setting-list-label">{{ trans('settings.users_role') }}</label>
|
||||
<p class="small">{{ trans('settings.users_role_desc') }}</p>
|
||||
<div class="mt-m">
|
||||
@include('form.role-checkboxes', ['name' => 'roles', 'roles' => $roles])
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(userCan('users-manage'))
|
||||
<div>
|
||||
<label for="role" class="setting-list-label">{{ trans('settings.users_role') }}</label>
|
||||
<p class="small">{{ trans('settings.users_role_desc') }}</p>
|
||||
<div class="mt-m">
|
||||
@include('form.role-checkboxes', ['name' => 'roles', 'roles' => $roles])
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($authMethod === 'standard')
|
||||
<div component="new-user-password">
|
||||
|
@ -64,7 +61,7 @@
|
|||
@endif
|
||||
|
||||
<div refs="new-user-password@input-container" @if(!isset($model)) style="display: none;" @endif>
|
||||
<p class="small">{{ trans('settings.users_password_desc') }}</p>
|
||||
<p class="small mb-none">{{ trans('settings.users_password_desc') }}</p>
|
||||
@if(isset($model))
|
||||
<p class="small">
|
||||
{{ trans('settings.users_password_warning') }}
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
@extends('layouts.simple')
|
||||
|
||||
@section('body')
|
||||
<div class="container small my-xl">
|
||||
|
||||
<section class="card content-wrap auto-height items-center justify-space-between gap-m flex-container-row wrap">
|
||||
<div class="flex min-width-m">
|
||||
<h2 class="list-heading">{{ trans('preferences.shortcuts_interface') }}</h2>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if(!user()->isGuest() && userCan('receive-notifications'))
|
||||
<section class="card content-wrap auto-height items-center justify-space-between gap-m flex-container-row wrap">
|
||||
<div class="flex min-width-m">
|
||||
<h2 class="list-heading">{{ trans('preferences.notifications') }}</h2>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@if(!user()->isGuest())
|
||||
<section class="card content-wrap auto-height items-center justify-space-between gap-m flex-container-row wrap">
|
||||
<div class="flex min-width-m">
|
||||
<h2 class="list-heading">{{ trans('settings.users_edit_profile') }}</h2>
|
||||
<p class="text-muted">{{ trans('preferences.profile_overview_desc') }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<a href="{{ user()->getEditUrl() }}" class="button outline">{{ trans('common.manage') }}</a>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@stop
|
|
@ -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
|
|
@ -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
|
|
@ -232,26 +232,33 @@ 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, 'redirect']);
|
||||
Route::get('/my-account/profile', [UserControllers\UserAccountController::class, 'showProfile']);
|
||||
Route::put('/my-account/profile', [UserControllers\UserAccountController::class, 'updateProfile']);
|
||||
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::get('/my-account/auth', [UserControllers\UserAccountController::class, 'showAuth']);
|
||||
Route::put('/my-account/auth/password', [UserControllers\UserAccountController::class, 'updatePassword']);
|
||||
Route::get('/my-account/delete', [UserControllers\UserAccountController::class, 'delete']);
|
||||
Route::delete('/my-account', [UserControllers\UserAccountController::class, 'destroy']);
|
||||
|
||||
// User Preference Endpoints
|
||||
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']);
|
||||
Route::post('/settings/users/{userId}/create-api-token', [UserApiTokenController::class, 'store']);
|
||||
Route::get('/settings/users/{userId}/api-tokens/{tokenId}', [UserApiTokenController::class, 'edit']);
|
||||
Route::put('/settings/users/{userId}/api-tokens/{tokenId}', [UserApiTokenController::class, 'update']);
|
||||
Route::get('/settings/users/{userId}/api-tokens/{tokenId}/delete', [UserApiTokenController::class, 'delete']);
|
||||
Route::delete('/settings/users/{userId}/api-tokens/{tokenId}', [UserApiTokenController::class, 'destroy']);
|
||||
Route::get('/api-tokens/{userId}/create', [UserApiTokenController::class, 'create']);
|
||||
Route::post('/api-tokens/{userId}/create', [UserApiTokenController::class, 'store']);
|
||||
Route::get('/api-tokens/{userId}/{tokenId}', [UserApiTokenController::class, 'edit']);
|
||||
Route::put('/api-tokens/{userId}/{tokenId}', [UserApiTokenController::class, 'update']);
|
||||
Route::get('/api-tokens/{userId}/{tokenId}/delete', [UserApiTokenController::class, 'delete']);
|
||||
Route::delete('/api-tokens/{userId}/{tokenId}', [UserApiTokenController::class, 'destroy']);
|
||||
|
||||
// Roles
|
||||
Route::get('/settings/roles', [UserControllers\RoleController::class, 'index']);
|
||||
|
|
|
@ -51,7 +51,7 @@ class WebhookCallTest extends TestCase
|
|||
{
|
||||
// This test must not fake the queue/bus since this covers an issue
|
||||
// around handling and serialization of items now deleted from the database.
|
||||
$this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);
|
||||
$webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);
|
||||
$this->mockHttpClient([new Response(500)]);
|
||||
|
||||
$user = $this->users->newUser();
|
||||
|
@ -61,8 +61,10 @@ class WebhookCallTest extends TestCase
|
|||
/** @var ApiToken $apiToken */
|
||||
$editor = $this->users->editor();
|
||||
$apiToken = ApiToken::factory()->create(['user_id' => $editor]);
|
||||
$resp = $this->delete($editor->getEditUrl('/api-tokens/' . $apiToken->id));
|
||||
$resp->assertRedirect($editor->getEditUrl('#api_tokens'));
|
||||
$this->delete($apiToken->getUrl())->assertRedirect();
|
||||
|
||||
$webhook->refresh();
|
||||
$this->assertEquals('Response status from endpoint was 500', $webhook->last_error);
|
||||
}
|
||||
|
||||
public function test_failed_webhook_call_logs_error()
|
||||
|
|
|
@ -18,7 +18,7 @@ class SocialAuthTest extends TestCase
|
|||
$user = User::factory()->make();
|
||||
|
||||
$this->setSettings(['registration-enabled' => 'true']);
|
||||
config(['GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc', 'APP_URL' => 'http://localhost']);
|
||||
config(['GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc']);
|
||||
|
||||
$mockSocialite = $this->mock(Factory::class);
|
||||
$mockSocialDriver = Mockery::mock(Provider::class);
|
||||
|
@ -45,7 +45,6 @@ class SocialAuthTest extends TestCase
|
|||
config([
|
||||
'GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc',
|
||||
'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc',
|
||||
'APP_URL' => 'http://localhost',
|
||||
]);
|
||||
|
||||
$mockSocialite = $this->mock(Factory::class);
|
||||
|
@ -86,12 +85,41 @@ class SocialAuthTest extends TestCase
|
|||
$this->assertActivityExists(ActivityType::AUTH_LOGIN, null, 'github; (' . $this->users->admin()->id . ') ' . $this->users->admin()->name);
|
||||
}
|
||||
|
||||
public function test_social_account_attach()
|
||||
{
|
||||
config([
|
||||
'GOOGLE_APP_ID' => 'abc123', 'GOOGLE_APP_SECRET' => '123abc',
|
||||
]);
|
||||
$editor = $this->users->editor();
|
||||
|
||||
$mockSocialite = $this->mock(Factory::class);
|
||||
$mockSocialDriver = Mockery::mock(Provider::class);
|
||||
$mockSocialUser = Mockery::mock(\Laravel\Socialite\Contracts\User::class);
|
||||
|
||||
$mockSocialUser->shouldReceive('getId')->twice()->andReturn('logintest123');
|
||||
$mockSocialUser->shouldReceive('getAvatar')->andReturn(null);
|
||||
|
||||
$mockSocialite->shouldReceive('driver')->twice()->with('google')->andReturn($mockSocialDriver);
|
||||
$mockSocialDriver->shouldReceive('redirect')->once()->andReturn(redirect('/login/service/google/callback'));
|
||||
$mockSocialDriver->shouldReceive('user')->once()->andReturn($mockSocialUser);
|
||||
|
||||
// Test login routes
|
||||
$resp = $this->actingAs($editor)->followingRedirects()->get('/login/service/google');
|
||||
$resp->assertSee('Access & Security');
|
||||
|
||||
// Test social callback with matching social account
|
||||
$this->assertDatabaseHas('social_accounts', [
|
||||
'user_id' => $editor->id,
|
||||
'driver' => 'google',
|
||||
'driver_id' => 'logintest123',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_social_account_detach()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
config([
|
||||
'GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc',
|
||||
'APP_URL' => 'http://localhost',
|
||||
]);
|
||||
|
||||
$socialAccount = SocialAccount::query()->forceCreate([
|
||||
|
@ -100,11 +128,11 @@ class SocialAuthTest extends TestCase
|
|||
'driver_id' => 'logintest123',
|
||||
]);
|
||||
|
||||
$resp = $this->actingAs($editor)->get($editor->getEditUrl());
|
||||
$resp = $this->actingAs($editor)->get('/my-account/auth');
|
||||
$this->withHtml($resp)->assertElementContains('form[action$="/login/service/github/detach"]', 'Disconnect Account');
|
||||
|
||||
$resp = $this->post('/login/service/github/detach');
|
||||
$resp->assertRedirect($editor->getEditUrl());
|
||||
$resp->assertRedirect('/my-account/auth#social-accounts');
|
||||
$resp = $this->followRedirects($resp);
|
||||
$resp->assertSee('Github account was successfully disconnected from your profile.');
|
||||
|
||||
|
@ -115,7 +143,6 @@ class SocialAuthTest extends TestCase
|
|||
{
|
||||
config([
|
||||
'services.google.client_id' => 'abc123', 'services.google.client_secret' => '123abc',
|
||||
'APP_URL' => 'http://localhost',
|
||||
]);
|
||||
|
||||
$user = User::factory()->make();
|
||||
|
@ -153,7 +180,7 @@ class SocialAuthTest extends TestCase
|
|||
{
|
||||
config([
|
||||
'services.google.client_id' => 'abc123', 'services.google.client_secret' => '123abc',
|
||||
'APP_URL' => 'http://localhost', 'services.google.auto_register' => true, 'services.google.auto_confirm' => true,
|
||||
'services.google.auto_register' => true, 'services.google.auto_confirm' => true,
|
||||
]);
|
||||
|
||||
$user = User::factory()->make();
|
||||
|
@ -191,7 +218,7 @@ class SocialAuthTest extends TestCase
|
|||
$user = User::factory()->make(['email' => 'nonameuser@example.com']);
|
||||
|
||||
$this->setSettings(['registration-enabled' => 'true']);
|
||||
config(['GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc', 'APP_URL' => 'http://localhost']);
|
||||
config(['GITHUB_APP_ID' => 'abc123', 'GITHUB_APP_SECRET' => '123abc']);
|
||||
|
||||
$mockSocialite = $this->mock(Factory::class);
|
||||
$mockSocialDriver = Mockery::mock(Provider::class);
|
||||
|
|
|
@ -44,14 +44,13 @@ class RolePermissionsTest extends TestCase
|
|||
|
||||
public function test_user_cannot_change_email_unless_they_have_manage_users_permission()
|
||||
{
|
||||
$userProfileUrl = '/settings/users/' . $this->user->id;
|
||||
$originalEmail = $this->user->email;
|
||||
$this->actingAs($this->user);
|
||||
|
||||
$resp = $this->get($userProfileUrl)
|
||||
->assertOk();
|
||||
$resp = $this->get('/my-account/profile')->assertOk();
|
||||
$this->withHtml($resp)->assertElementExists('input[name=email][disabled]');
|
||||
$this->put($userProfileUrl, [
|
||||
$resp->assertSee('Unfortunately you don\'t have permission to change your email address.');
|
||||
$this->put('/my-account/profile', [
|
||||
'name' => 'my_new_name',
|
||||
'email' => 'new_email@example.com',
|
||||
]);
|
||||
|
@ -63,11 +62,12 @@ class RolePermissionsTest extends TestCase
|
|||
|
||||
$this->permissions->grantUserRolePermissions($this->user, ['users-manage']);
|
||||
|
||||
$resp = $this->get($userProfileUrl)
|
||||
->assertOk();
|
||||
$this->withHtml($resp)->assertElementNotExists('input[name=email][disabled]')
|
||||
$resp = $this->get('/my-account/profile')->assertOk();
|
||||
$this->withHtml($resp)
|
||||
->assertElementNotExists('input[name=email][disabled]')
|
||||
->assertElementExists('input[name=email]');
|
||||
$this->put($userProfileUrl, [
|
||||
|
||||
$this->put('/my-account/profile', [
|
||||
'name' => 'my_new_name_2',
|
||||
'email' => 'new_email@example.com',
|
||||
]);
|
||||
|
|
|
@ -607,7 +607,7 @@ class ImageTest extends TestCase
|
|||
$this->actingAs($editor);
|
||||
|
||||
$file = $this->getTestProfileImage();
|
||||
$this->call('PUT', '/settings/users/' . $editor->id, [], [], ['profile_image' => $file], []);
|
||||
$this->call('PUT', '/my-account/profile', [], [], ['profile_image' => $file], []);
|
||||
|
||||
$profileImages = Image::where('type', '=', 'user')->where('created_by', '=', $editor->id)->get();
|
||||
$this->assertTrue($profileImages->count() === 1, 'Found profile images does not match upload count');
|
||||
|
@ -615,7 +615,7 @@ class ImageTest extends TestCase
|
|||
$imagePath = public_path($profileImages->first()->path);
|
||||
$this->assertTrue(file_exists($imagePath));
|
||||
|
||||
$userDelete = $this->asAdmin()->delete("/settings/users/{$editor->id}");
|
||||
$userDelete = $this->asAdmin()->delete($editor->getEditUrl());
|
||||
$userDelete->assertStatus(302);
|
||||
|
||||
$this->assertDatabaseMissing('images', [
|
||||
|
|
|
@ -5,25 +5,26 @@ namespace Tests\User;
|
|||
use BookStack\Activity\ActivityType;
|
||||
use BookStack\Api\ApiToken;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserApiTokenTest extends TestCase
|
||||
{
|
||||
protected $testTokenData = [
|
||||
protected array $testTokenData = [
|
||||
'name' => 'My test API token',
|
||||
'expires_at' => '2050-04-01',
|
||||
];
|
||||
|
||||
public function test_tokens_section_not_visible_without_access_api_permission()
|
||||
public function test_tokens_section_not_visible_in_my_account_without_access_api_permission()
|
||||
{
|
||||
$user = $this->users->viewer();
|
||||
|
||||
$resp = $this->actingAs($user)->get($user->getEditUrl());
|
||||
$resp = $this->actingAs($user)->get('/my-account/auth');
|
||||
$resp->assertDontSeeText('API Tokens');
|
||||
|
||||
$this->permissions->grantUserRolePermissions($user, ['access-api']);
|
||||
|
||||
$resp = $this->actingAs($user)->get($user->getEditUrl());
|
||||
$resp = $this->actingAs($user)->get('/my-account/auth');
|
||||
$resp->assertSeeText('API Tokens');
|
||||
$resp->assertSeeText('Create Token');
|
||||
}
|
||||
|
@ -43,14 +44,14 @@ class UserApiTokenTest extends TestCase
|
|||
{
|
||||
$editor = $this->users->editor();
|
||||
|
||||
$resp = $this->asAdmin()->get($editor->getEditUrl('/create-api-token'));
|
||||
$resp = $this->asAdmin()->get("/api-tokens/{$editor->id}/create");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee('Create API Token');
|
||||
$resp->assertSee('Token Secret');
|
||||
|
||||
$resp = $this->post($editor->getEditUrl('/create-api-token'), $this->testTokenData);
|
||||
$resp = $this->post("/api-tokens/{$editor->id}/create", $this->testTokenData);
|
||||
$token = ApiToken::query()->latest()->first();
|
||||
$resp->assertRedirect($editor->getEditUrl('/api-tokens/' . $token->id));
|
||||
$resp->assertRedirect("/api-tokens/{$editor->id}/{$token->id}");
|
||||
$this->assertDatabaseHas('api_tokens', [
|
||||
'user_id' => $editor->id,
|
||||
'name' => $this->testTokenData['name'],
|
||||
|
@ -63,7 +64,7 @@ class UserApiTokenTest extends TestCase
|
|||
$this->assertDatabaseMissing('api_tokens', [
|
||||
'secret' => $secret,
|
||||
]);
|
||||
$this->assertTrue(\Hash::check($secret, $token->secret));
|
||||
$this->assertTrue(Hash::check($secret, $token->secret));
|
||||
|
||||
$this->assertTrue(strlen($token->token_id) === 32);
|
||||
$this->assertTrue(strlen($secret) === 32);
|
||||
|
@ -75,7 +76,10 @@ class UserApiTokenTest extends TestCase
|
|||
public function test_create_with_no_expiry_sets_expiry_hundred_years_away()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$this->asAdmin()->post($editor->getEditUrl('/create-api-token'), ['name' => 'No expiry token', 'expires_at' => '']);
|
||||
|
||||
$resp = $this->asAdmin()->post("/api-tokens/{$editor->id}/create", ['name' => 'No expiry token', 'expires_at' => '']);
|
||||
$resp->assertRedirect();
|
||||
|
||||
$token = ApiToken::query()->latest()->first();
|
||||
|
||||
$over = Carbon::now()->addYears(101);
|
||||
|
@ -89,7 +93,9 @@ class UserApiTokenTest extends TestCase
|
|||
public function test_created_token_displays_on_profile_page()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$this->asAdmin()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData);
|
||||
$resp = $this->asAdmin()->post("/api-tokens/{$editor->id}/create", $this->testTokenData);
|
||||
$resp->assertRedirect();
|
||||
|
||||
$token = ApiToken::query()->latest()->first();
|
||||
|
||||
$resp = $this->get($editor->getEditUrl());
|
||||
|
@ -102,28 +108,29 @@ class UserApiTokenTest extends TestCase
|
|||
public function test_secret_shown_once_after_creation()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$resp = $this->asAdmin()->followingRedirects()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData);
|
||||
$resp = $this->asAdmin()->followingRedirects()->post("/api-tokens/{$editor->id}/create", $this->testTokenData);
|
||||
$resp->assertSeeText('Token Secret');
|
||||
|
||||
$token = ApiToken::query()->latest()->first();
|
||||
$this->assertNull(session('api-token-secret:' . $token->id));
|
||||
|
||||
$resp = $this->get($editor->getEditUrl('/api-tokens/' . $token->id));
|
||||
$resp = $this->get("/api-tokens/{$editor->id}/{$token->id}");
|
||||
$resp->assertOk();
|
||||
$resp->assertDontSeeText('Client Secret');
|
||||
}
|
||||
|
||||
public function test_token_update()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$this->asAdmin()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData);
|
||||
$this->asAdmin()->post("/api-tokens/{$editor->id}/create", $this->testTokenData);
|
||||
$token = ApiToken::query()->latest()->first();
|
||||
$updateData = [
|
||||
'name' => 'My updated token',
|
||||
'expires_at' => '2011-01-01',
|
||||
];
|
||||
|
||||
$resp = $this->put($editor->getEditUrl('/api-tokens/' . $token->id), $updateData);
|
||||
$resp->assertRedirect($editor->getEditUrl('/api-tokens/' . $token->id));
|
||||
$resp = $this->put("/api-tokens/{$editor->id}/{$token->id}", $updateData);
|
||||
$resp->assertRedirect("/api-tokens/{$editor->id}/{$token->id}");
|
||||
|
||||
$this->assertDatabaseHas('api_tokens', array_merge($updateData, ['id' => $token->id]));
|
||||
$this->assertSessionHas('success');
|
||||
|
@ -133,13 +140,13 @@ class UserApiTokenTest extends TestCase
|
|||
public function test_token_update_with_blank_expiry_sets_to_hundred_years_away()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$this->asAdmin()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData);
|
||||
$this->asAdmin()->post("/api-tokens/{$editor->id}/create", $this->testTokenData);
|
||||
$token = ApiToken::query()->latest()->first();
|
||||
|
||||
$resp = $this->put($editor->getEditUrl('/api-tokens/' . $token->id), [
|
||||
$this->put("/api-tokens/{$editor->id}/{$token->id}", [
|
||||
'name' => 'My updated token',
|
||||
'expires_at' => '',
|
||||
]);
|
||||
])->assertRedirect();
|
||||
$token->refresh();
|
||||
|
||||
$over = Carbon::now()->addYears(101);
|
||||
|
@ -153,15 +160,15 @@ class UserApiTokenTest extends TestCase
|
|||
public function test_token_delete()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$this->asAdmin()->post($editor->getEditUrl('/create-api-token'), $this->testTokenData);
|
||||
$this->asAdmin()->post("/api-tokens/{$editor->id}/create", $this->testTokenData);
|
||||
$token = ApiToken::query()->latest()->first();
|
||||
|
||||
$tokenUrl = $editor->getEditUrl('/api-tokens/' . $token->id);
|
||||
$tokenUrl = "/api-tokens/{$editor->id}/{$token->id}";
|
||||
|
||||
$resp = $this->get($tokenUrl . '/delete');
|
||||
$resp->assertSeeText('Delete Token');
|
||||
$resp->assertSeeText($token->name);
|
||||
$this->withHtml($resp)->assertElementExists('form[action="' . $tokenUrl . '"]');
|
||||
$this->withHtml($resp)->assertElementExists('form[action$="' . $tokenUrl . '"]');
|
||||
|
||||
$resp = $this->delete($tokenUrl);
|
||||
$resp->assertRedirect($editor->getEditUrl('#api_tokens'));
|
||||
|
@ -175,15 +182,46 @@ class UserApiTokenTest extends TestCase
|
|||
$editor = $this->users->editor();
|
||||
$this->permissions->grantUserRolePermissions($editor, ['users-manage']);
|
||||
|
||||
$this->asAdmin()->post($viewer->getEditUrl('/create-api-token'), $this->testTokenData);
|
||||
$this->asAdmin()->post("/api-tokens/{$viewer->id}/create", $this->testTokenData);
|
||||
$token = ApiToken::query()->latest()->first();
|
||||
|
||||
$resp = $this->actingAs($editor)->get($viewer->getEditUrl('/api-tokens/' . $token->id));
|
||||
$resp = $this->actingAs($editor)->get("/api-tokens/{$viewer->id}/{$token->id}");
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSeeText('Delete Token');
|
||||
|
||||
$resp = $this->actingAs($editor)->delete($viewer->getEditUrl('/api-tokens/' . $token->id));
|
||||
$resp = $this->actingAs($editor)->delete("/api-tokens/{$viewer->id}/{$token->id}");
|
||||
$resp->assertRedirect($viewer->getEditUrl('#api_tokens'));
|
||||
$this->assertDatabaseMissing('api_tokens', ['id' => $token->id]);
|
||||
}
|
||||
|
||||
public function test_return_routes_change_depending_on_entry_context()
|
||||
{
|
||||
$user = $this->users->admin();
|
||||
$returnByContext = [
|
||||
'settings' => url("/settings/users/{$user->id}/#api_tokens"),
|
||||
'my-account' => url('/my-account/auth#api_tokens'),
|
||||
];
|
||||
|
||||
foreach ($returnByContext as $context => $returnUrl) {
|
||||
$resp = $this->actingAs($user)->get("/api-tokens/{$user->id}/create?context={$context}");
|
||||
$this->withHtml($resp)->assertLinkExists($returnUrl, 'Cancel');
|
||||
|
||||
$this->post("/api-tokens/{$user->id}/create", $this->testTokenData);
|
||||
$token = $user->apiTokens()->latest()->first();
|
||||
|
||||
$resp = $this->get($token->getUrl());
|
||||
$this->withHtml($resp)->assertLinkExists($returnUrl, 'Back');
|
||||
|
||||
$resp = $this->delete($token->getUrl());
|
||||
$resp->assertRedirect($returnUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_context_assumed_for_editing_tokens_of_another_user()
|
||||
{
|
||||
$user = $this->users->viewer();
|
||||
|
||||
$resp = $this->asAdmin()->get("/api-tokens/{$user->id}/create?context=my-account");
|
||||
$this->withHtml($resp)->assertLinkExists($user->getEditUrl('#api_tokens'), 'Cancel');
|
||||
}
|
||||
}
|
||||
|
|
339
tests/User/UserMyAccountTest.php
Normal file
339
tests/User/UserMyAccountTest.php
Normal file
|
@ -0,0 +1,339 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\User;
|
||||
|
||||
use BookStack\Access\Mfa\MfaValue;
|
||||
use BookStack\Activity\Tools\UserEntityWatchOptions;
|
||||
use BookStack\Activity\WatchLevels;
|
||||
use BookStack\Api\ApiToken;
|
||||
use BookStack\Uploads\Image;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UserMyAccountTest extends TestCase
|
||||
{
|
||||
public function test_index_view()
|
||||
{
|
||||
$resp = $this->asEditor()->get('/my-account');
|
||||
$resp->assertRedirect('/my-account/profile');
|
||||
}
|
||||
|
||||
public function test_views_not_accessible_to_guest_user()
|
||||
{
|
||||
$categories = ['profile', 'auth', 'shortcuts', 'notifications', ''];
|
||||
$this->setSettings(['app-public' => 'true']);
|
||||
|
||||
$this->permissions->grantUserRolePermissions($this->users->guest(), ['receive-notifications']);
|
||||
|
||||
foreach ($categories as $category) {
|
||||
$resp = $this->get('/my-account/' . $category);
|
||||
$resp->assertRedirect('/');
|
||||
}
|
||||
}
|
||||
|
||||
public function test_profile_updating()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/my-account/profile');
|
||||
$resp->assertSee('Profile Details');
|
||||
|
||||
$html = $this->withHtml($resp);
|
||||
$html->assertFieldHasValue('name', $editor->name);
|
||||
$html->assertFieldHasValue('email', $editor->email);
|
||||
|
||||
$resp = $this->put('/my-account/profile', [
|
||||
'name' => 'Barryius',
|
||||
'email' => 'barryius@example.com',
|
||||
'language' => 'fr',
|
||||
]);
|
||||
|
||||
$resp->assertRedirect('/my-account/profile');
|
||||
$this->assertDatabaseHas('users', [
|
||||
'name' => 'Barryius',
|
||||
'email' => $editor->email, // No email change due to not having permissions
|
||||
]);
|
||||
$this->assertEquals(setting()->getUser($editor, 'language'), 'fr');
|
||||
}
|
||||
|
||||
public function test_profile_user_avatar_update_and_reset()
|
||||
{
|
||||
$user = $this->users->viewer();
|
||||
$avatarFile = $this->files->uploadedImage('avatar-icon.png');
|
||||
|
||||
$this->assertEquals(0, $user->image_id);
|
||||
|
||||
$upload = $this->actingAs($user)->call('PUT', "/my-account/profile", [
|
||||
'name' => 'Barry Scott',
|
||||
], [], ['profile_image' => $avatarFile], []);
|
||||
$upload->assertRedirect('/my-account/profile');
|
||||
|
||||
|
||||
$user->refresh();
|
||||
$this->assertNotEquals(0, $user->image_id);
|
||||
/** @var Image $image */
|
||||
$image = Image::query()->findOrFail($user->image_id);
|
||||
$this->assertFileExists(public_path($image->path));
|
||||
|
||||
$reset = $this->put("/my-account/profile", [
|
||||
'name' => 'Barry Scott',
|
||||
'profile_image_reset' => 'true',
|
||||
]);
|
||||
$upload->assertRedirect('/my-account/profile');
|
||||
|
||||
$user->refresh();
|
||||
$this->assertFileDoesNotExist(public_path($image->path));
|
||||
$this->assertEquals(0, $user->image_id);
|
||||
}
|
||||
|
||||
public function test_profile_admin_options_link_shows_if_permissions_allow()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/my-account/profile');
|
||||
$resp->assertDontSee('Administrator Options');
|
||||
$this->withHtml($resp)->assertLinkNotExists(url("/settings/users/{$editor->id}"));
|
||||
|
||||
$this->permissions->grantUserRolePermissions($editor, ['users-manage']);
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/my-account/profile');
|
||||
$resp->assertSee('Administrator Options');
|
||||
$this->withHtml($resp)->assertLinkExists(url("/settings/users/{$editor->id}"));
|
||||
}
|
||||
|
||||
public function test_profile_self_delete()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/my-account/profile');
|
||||
$this->withHtml($resp)->assertLinkExists(url('/my-account/delete'), 'Delete Account');
|
||||
|
||||
$resp = $this->get('/my-account/delete');
|
||||
$resp->assertSee('Delete My Account');
|
||||
$this->withHtml($resp)->assertElementContains('form[action$="/my-account"] button', 'Confirm');
|
||||
|
||||
$resp = $this->delete('/my-account');
|
||||
$resp->assertRedirect('/');
|
||||
|
||||
$this->assertDatabaseMissing('users', ['id' => $editor->id]);
|
||||
}
|
||||
|
||||
public function test_profile_self_delete_shows_ownership_migration_if_can_manage_users()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/my-account/delete');
|
||||
$resp->assertDontSee('Migrate Ownership');
|
||||
|
||||
$this->permissions->grantUserRolePermissions($editor, ['users-manage']);
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/my-account/delete');
|
||||
$resp->assertSee('Migrate Ownership');
|
||||
}
|
||||
|
||||
public function test_auth_password_change()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/my-account/auth');
|
||||
$resp->assertSee('Change Password');
|
||||
$this->withHtml($resp)->assertElementExists('form[action$="/my-account/auth/password"]');
|
||||
|
||||
$password = Str::random();
|
||||
$resp = $this->put('/my-account/auth/password', [
|
||||
'password' => $password,
|
||||
'password-confirm' => $password,
|
||||
]);
|
||||
$resp->assertRedirect('/my-account/auth');
|
||||
|
||||
$editor->refresh();
|
||||
$this->assertTrue(Hash::check($password, $editor->password));
|
||||
}
|
||||
|
||||
public function test_auth_password_change_hides_if_not_using_email_auth()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/my-account/auth');
|
||||
$resp->assertSee('Change Password');
|
||||
|
||||
config()->set('auth.method', 'oidc');
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/my-account/auth');
|
||||
$resp->assertDontSee('Change Password');
|
||||
}
|
||||
|
||||
public function test_auth_page_has_mfa_links()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$resp = $this->actingAs($editor)->get('/my-account/auth');
|
||||
$resp->assertSee('0 methods configured');
|
||||
$this->withHtml($resp)->assertLinkExists(url('/mfa/setup'));
|
||||
|
||||
MfaValue::upsertWithValue($editor, 'totp', 'testval');
|
||||
|
||||
$resp = $this->get('/my-account/auth');
|
||||
$resp->assertSee('1 method configured');
|
||||
}
|
||||
|
||||
public function test_auth_page_api_tokens()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$resp = $this->actingAs($editor)->get('/my-account/auth');
|
||||
$resp->assertSee('API Tokens');
|
||||
$this->withHtml($resp)->assertLinkExists(url("/api-tokens/{$editor->id}/create?context=my-account"));
|
||||
|
||||
ApiToken::factory()->create(['user_id' => $editor->id, 'name' => 'My great token']);
|
||||
$editor->unsetRelations();
|
||||
|
||||
$resp = $this->get('/my-account/auth');
|
||||
$resp->assertSee('My great token');
|
||||
}
|
||||
|
||||
public function test_interface_shortcuts_updating()
|
||||
{
|
||||
$this->asEditor();
|
||||
|
||||
// View preferences with defaults
|
||||
$resp = $this->get('/my-account/shortcuts');
|
||||
$resp->assertSee('UI Shortcut Preferences');
|
||||
|
||||
$html = $this->withHtml($resp);
|
||||
$html->assertFieldHasValue('enabled', 'false');
|
||||
$html->assertFieldHasValue('shortcut[home_view]', '1');
|
||||
|
||||
// Update preferences
|
||||
$resp = $this->put('/my-account/shortcuts', [
|
||||
'enabled' => 'true',
|
||||
'shortcut' => ['home_view' => 'Ctrl + 1'],
|
||||
]);
|
||||
|
||||
$resp->assertRedirect('/my-account/shortcuts');
|
||||
$resp->assertSessionHas('success', 'Shortcut preferences have been updated!');
|
||||
|
||||
// View updates to preferences page
|
||||
$resp = $this->get('/my-account/shortcuts');
|
||||
$html = $this->withHtml($resp);
|
||||
$html->assertFieldHasValue('enabled', 'true');
|
||||
$html->assertFieldHasValue('shortcut[home_view]', 'Ctrl + 1');
|
||||
}
|
||||
|
||||
public function test_body_has_shortcuts_component_when_active()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$this->actingAs($editor);
|
||||
|
||||
$this->withHtml($this->get('/'))->assertElementNotExists('body[component="shortcuts"]');
|
||||
|
||||
setting()->putUser($editor, 'ui-shortcuts-enabled', 'true');
|
||||
$this->withHtml($this->get('/'))->assertElementExists('body[component="shortcuts"]');
|
||||
}
|
||||
|
||||
public function test_notification_routes_requires_notification_permission()
|
||||
{
|
||||
$viewer = $this->users->viewer();
|
||||
$resp = $this->actingAs($viewer)->get('/my-account/notifications');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->actingAs($viewer)->get('/my-account/profile');
|
||||
$resp->assertDontSeeText('Notification Preferences');
|
||||
|
||||
$resp = $this->put('/my-account/notifications');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$this->permissions->grantUserRolePermissions($viewer, ['receive-notifications']);
|
||||
$resp = $this->get('/my-account/notifications');
|
||||
$resp->assertOk();
|
||||
$resp->assertSee('Notification Preferences');
|
||||
}
|
||||
|
||||
public function test_notification_preferences_updating()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
|
||||
// View preferences with defaults
|
||||
$resp = $this->actingAs($editor)->get('/my-account/notifications');
|
||||
$resp->assertSee('Notification Preferences');
|
||||
|
||||
$html = $this->withHtml($resp);
|
||||
$html->assertFieldHasValue('preferences[comment-replies]', 'false');
|
||||
|
||||
// Update preferences
|
||||
$resp = $this->put('/my-account/notifications', [
|
||||
'preferences' => ['comment-replies' => 'true'],
|
||||
]);
|
||||
|
||||
$resp->assertRedirect('/my-account/notifications');
|
||||
$resp->assertSessionHas('success', 'Notification preferences have been updated!');
|
||||
|
||||
// View updates to preferences page
|
||||
$resp = $this->get('/my-account/notifications');
|
||||
$html = $this->withHtml($resp);
|
||||
$html->assertFieldHasValue('preferences[comment-replies]', 'true');
|
||||
}
|
||||
|
||||
public function test_notification_preferences_show_watches()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$book = $this->entities->book();
|
||||
|
||||
$options = new UserEntityWatchOptions($editor, $book);
|
||||
$options->updateLevelByValue(WatchLevels::COMMENTS);
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/my-account/notifications');
|
||||
$resp->assertSee($book->name);
|
||||
$resp->assertSee('All Page Updates & Comments');
|
||||
|
||||
$options->updateLevelByValue(WatchLevels::DEFAULT);
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/my-account/notifications');
|
||||
$resp->assertDontSee($book->name);
|
||||
$resp->assertDontSee('All Page Updates & Comments');
|
||||
}
|
||||
|
||||
public function test_notification_preferences_dont_error_on_deleted_items()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$book = $this->entities->book();
|
||||
|
||||
$options = new UserEntityWatchOptions($editor, $book);
|
||||
$options->updateLevelByValue(WatchLevels::COMMENTS);
|
||||
|
||||
$this->actingAs($editor)->delete($book->getUrl());
|
||||
$book->refresh();
|
||||
$this->assertNotNull($book->deleted_at);
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/my-account/notifications');
|
||||
$resp->assertOk();
|
||||
$resp->assertDontSee($book->name);
|
||||
}
|
||||
|
||||
public function test_notification_preferences_not_accessible_to_guest()
|
||||
{
|
||||
$this->setSettings(['app-public' => 'true']);
|
||||
$guest = $this->users->guest();
|
||||
$this->permissions->grantUserRolePermissions($guest, ['receive-notifications']);
|
||||
|
||||
$resp = $this->get('/my-account/notifications');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->put('/my-account/notifications', [
|
||||
'preferences' => ['comment-replies' => 'true'],
|
||||
]);
|
||||
$this->assertPermissionError($resp);
|
||||
}
|
||||
|
||||
public function test_notification_comment_options_only_exist_if_comments_active()
|
||||
{
|
||||
$resp = $this->asEditor()->get('/my-account/notifications');
|
||||
$resp->assertSee('Notify upon comments');
|
||||
$resp->assertSee('Notify upon replies');
|
||||
|
||||
setting()->put('app-disable-comments', true);
|
||||
|
||||
$resp = $this->get('/my-account/notifications');
|
||||
$resp->assertDontSee('Notify upon comments');
|
||||
$resp->assertDontSee('Notify upon replies');
|
||||
}
|
||||
}
|
|
@ -8,167 +8,6 @@ use Tests\TestCase;
|
|||
|
||||
class UserPreferencesTest extends TestCase
|
||||
{
|
||||
public function test_index_view()
|
||||
{
|
||||
$resp = $this->asEditor()->get('/preferences');
|
||||
$resp->assertOk();
|
||||
$resp->assertSee('Interface Keyboard Shortcuts');
|
||||
$resp->assertSee('Edit Profile');
|
||||
}
|
||||
|
||||
public function test_index_view_accessible_but_without_profile_and_notifications_for_guest_user()
|
||||
{
|
||||
$this->setSettings(['app-public' => 'true']);
|
||||
$this->permissions->grantUserRolePermissions($this->users->guest(), ['receive-notifications']);
|
||||
$resp = $this->get('/preferences');
|
||||
$resp->assertOk();
|
||||
$resp->assertSee('Interface Keyboard Shortcuts');
|
||||
$resp->assertDontSee('Edit Profile');
|
||||
$resp->assertDontSee('Notification');
|
||||
}
|
||||
public function test_interface_shortcuts_updating()
|
||||
{
|
||||
$this->asEditor();
|
||||
|
||||
// View preferences with defaults
|
||||
$resp = $this->get('/preferences/shortcuts');
|
||||
$resp->assertSee('Interface Keyboard Shortcuts');
|
||||
|
||||
$html = $this->withHtml($resp);
|
||||
$html->assertFieldHasValue('enabled', 'false');
|
||||
$html->assertFieldHasValue('shortcut[home_view]', '1');
|
||||
|
||||
// Update preferences
|
||||
$resp = $this->put('/preferences/shortcuts', [
|
||||
'enabled' => 'true',
|
||||
'shortcut' => ['home_view' => 'Ctrl + 1'],
|
||||
]);
|
||||
|
||||
$resp->assertRedirect('/preferences/shortcuts');
|
||||
$resp->assertSessionHas('success', 'Shortcut preferences have been updated!');
|
||||
|
||||
// View updates to preferences page
|
||||
$resp = $this->get('/preferences/shortcuts');
|
||||
$html = $this->withHtml($resp);
|
||||
$html->assertFieldHasValue('enabled', 'true');
|
||||
$html->assertFieldHasValue('shortcut[home_view]', 'Ctrl + 1');
|
||||
}
|
||||
|
||||
public function test_body_has_shortcuts_component_when_active()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$this->actingAs($editor);
|
||||
|
||||
$this->withHtml($this->get('/'))->assertElementNotExists('body[component="shortcuts"]');
|
||||
|
||||
setting()->putUser($editor, 'ui-shortcuts-enabled', 'true');
|
||||
$this->withHtml($this->get('/'))->assertElementExists('body[component="shortcuts"]');
|
||||
}
|
||||
|
||||
public function test_notification_routes_requires_notification_permission()
|
||||
{
|
||||
$viewer = $this->users->viewer();
|
||||
$resp = $this->actingAs($viewer)->get('/preferences/notifications');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->put('/preferences/notifications');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$this->permissions->grantUserRolePermissions($viewer, ['receive-notifications']);
|
||||
$resp = $this->get('/preferences/notifications');
|
||||
$resp->assertOk();
|
||||
$resp->assertSee('Notification Preferences');
|
||||
}
|
||||
|
||||
public function test_notification_preferences_updating()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
|
||||
// View preferences with defaults
|
||||
$resp = $this->actingAs($editor)->get('/preferences/notifications');
|
||||
$resp->assertSee('Notification Preferences');
|
||||
|
||||
$html = $this->withHtml($resp);
|
||||
$html->assertFieldHasValue('preferences[comment-replies]', 'false');
|
||||
|
||||
// Update preferences
|
||||
$resp = $this->put('/preferences/notifications', [
|
||||
'preferences' => ['comment-replies' => 'true'],
|
||||
]);
|
||||
|
||||
$resp->assertRedirect('/preferences/notifications');
|
||||
$resp->assertSessionHas('success', 'Notification preferences have been updated!');
|
||||
|
||||
// View updates to preferences page
|
||||
$resp = $this->get('/preferences/notifications');
|
||||
$html = $this->withHtml($resp);
|
||||
$html->assertFieldHasValue('preferences[comment-replies]', 'true');
|
||||
}
|
||||
|
||||
public function test_notification_preferences_show_watches()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$book = $this->entities->book();
|
||||
|
||||
$options = new UserEntityWatchOptions($editor, $book);
|
||||
$options->updateLevelByValue(WatchLevels::COMMENTS);
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/preferences/notifications');
|
||||
$resp->assertSee($book->name);
|
||||
$resp->assertSee('All Page Updates & Comments');
|
||||
|
||||
$options->updateLevelByValue(WatchLevels::DEFAULT);
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/preferences/notifications');
|
||||
$resp->assertDontSee($book->name);
|
||||
$resp->assertDontSee('All Page Updates & Comments');
|
||||
}
|
||||
|
||||
public function test_notification_preferences_dont_error_on_deleted_items()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
$book = $this->entities->book();
|
||||
|
||||
$options = new UserEntityWatchOptions($editor, $book);
|
||||
$options->updateLevelByValue(WatchLevels::COMMENTS);
|
||||
|
||||
$this->actingAs($editor)->delete($book->getUrl());
|
||||
$book->refresh();
|
||||
$this->assertNotNull($book->deleted_at);
|
||||
|
||||
$resp = $this->actingAs($editor)->get('/preferences/notifications');
|
||||
$resp->assertOk();
|
||||
$resp->assertDontSee($book->name);
|
||||
}
|
||||
|
||||
public function test_notification_preferences_not_accessible_to_guest()
|
||||
{
|
||||
$this->setSettings(['app-public' => 'true']);
|
||||
$guest = $this->users->guest();
|
||||
$this->permissions->grantUserRolePermissions($guest, ['receive-notifications']);
|
||||
|
||||
$resp = $this->get('/preferences/notifications');
|
||||
$this->assertPermissionError($resp);
|
||||
|
||||
$resp = $this->put('/preferences/notifications', [
|
||||
'preferences' => ['comment-replies' => 'true'],
|
||||
]);
|
||||
$this->assertPermissionError($resp);
|
||||
}
|
||||
|
||||
public function test_notification_comment_options_only_exist_if_comments_active()
|
||||
{
|
||||
$resp = $this->asEditor()->get('/preferences/notifications');
|
||||
$resp->assertSee('Notify upon comments');
|
||||
$resp->assertSee('Notify upon replies');
|
||||
|
||||
setting()->put('app-disable-comments', true);
|
||||
|
||||
$resp = $this->get('/preferences/notifications');
|
||||
$resp->assertDontSee('Notify upon comments');
|
||||
$resp->assertDontSee('Notify upon replies');
|
||||
}
|
||||
|
||||
public function test_update_sort_preference()
|
||||
{
|
||||
$editor = $this->users->editor();
|
||||
|
|
Loading…
Add table
Reference in a new issue