Users: Built out auth page for my-account section

This commit is contained in:
Dan Brown 2023-10-17 17:38:07 +01:00
parent a9d0f36766
commit a868012048
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
12 changed files with 174 additions and 29 deletions

View File

@ -214,6 +214,7 @@ class SocialAuthService
/**
* Gets the names of the active social drivers.
* @returns array<string, string>
*/
public function getActiveDrivers(): array
{

View File

@ -2,18 +2,25 @@
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\Users\UserRepo;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Validation\Rules\Password;
class UserAccountController extends Controller
{
public function __construct(
protected UserRepo $userRepo
protected UserRepo $userRepo,
) {
$this->middleware(function (Request $request, Closure $next) {
$this->preventGuestAccess();
return $next($request);
});
}
/**
@ -21,8 +28,7 @@ class UserAccountController extends Controller
*/
public function index()
{
$guest = user()->isGuest();
$mfaMethods = $guest ? [] : user()->mfaValues->groupBy('method');
$mfaMethods = user()->mfaValues->groupBy('method');
return view('users.account.index', [
'mfaMethods' => $mfaMethods,
@ -40,6 +46,7 @@ class UserAccountController extends Controller
$this->setPageTitle(trans('preferences.shortcuts_interface'));
return view('users.account.shortcuts', [
'category' => 'shortcuts',
'shortcuts' => $shortcuts,
'enabled' => $enabled,
]);
@ -68,7 +75,6 @@ class UserAccountController extends Controller
public function showNotifications(PermissionApplicator $permissions)
{
$this->checkPermission('receive-notifications');
$this->preventGuestAccess();
$preferences = (new UserNotificationPreferences(user()));
@ -79,6 +85,7 @@ class UserAccountController extends Controller
$this->setPageTitle(trans('preferences.notifications'));
return view('users.account.notifications', [
'category' => 'notifications',
'preferences' => $preferences,
'watches' => $watches,
]);
@ -90,7 +97,6 @@ class UserAccountController extends Controller
public function updateNotifications(Request $request)
{
$this->checkPermission('receive-notifications');
$this->preventGuestAccess();
$data = $this->validate($request, [
'preferences' => ['required', 'array'],
'preferences.*' => ['required', 'string'],
@ -102,4 +108,42 @@ class UserAccountController extends Controller
return redirect('/my-account/notifications');
}
/**
* Show the view for the "Access & Security" account options.
*/
public function showAuth(SocialAuthService $socialAuthService)
{
$mfaMethods = user()->mfaValues->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');
}
}

View File

@ -29,5 +29,11 @@ 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.',
'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_overview_desc' => ' Manage your user profile details including preferred language and authentication options.',
];

View File

@ -194,7 +194,7 @@ return [
'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_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,14 @@ 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_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',

View 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

Width:  |  Height:  |  Size: 303 B

View File

@ -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>

View 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()])
@endif
@stop

View File

@ -9,9 +9,10 @@
<div class="sticky-top-m">
<h5>{{ trans('preferences.my_account') }}</h5>
<nav class="active-link-list in-sidebar">
<a href="{{ url('/my-account/shortcuts') }}" class="{{ 'shortcuts' === 'shortcuts' ? 'active' : '' }}">@icon('shortcuts') {{ trans('preferences.shortcuts_interface') }}</a>
<a href="{{ url('/my-account/notifications') }}" class="{{ '' === 'notifications' ? 'active' : '' }}">@icon('notifications') {{ trans('preferences.notifications') }}</a>
<a href="{{ url('/my-account/auth') }}" class="{{ '' === 'auth' ? 'active' : '' }}">@icon('lock') {{ 'Access & Security' }}</a>
<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>
<a href="{{ url('/my-account/notifications') }}" class="{{ $category === 'notifications' ? 'active' : '' }}">@icon('notifications') {{ trans('preferences.notifications') }}</a>
</nav>
</div>
</div>

View File

@ -8,6 +8,7 @@
@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)

View File

@ -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,29 +71,29 @@
</section>
@if(user()->id === $user->id && count($activeSocialDrivers) > 0)
@if(count($activeSocialDrivers) > 0)
<section class="card content-wrap auto-height">
<div class="flex-container-row items-center justify-space-between wrap">
<h2 class="list-heading">{{ trans('settings.users_social_accounts') }}</h2>
<p class="text-muted">{{ 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>
@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 => $driverName)
<div class="text-center mb-m">
<div role="presentation">@icon('auth/'. $driver, ['style' => 'width: 56px;height: 56px;'])</div>
<p class="my-none bold">{{ $driverName }}</p>
@if($user->hasSocialAccount($driver))
<p class="text-pos bold text-small my-none">Connected</p>
@else
<p class="text-neg bold text-small my-none">Disconnected</p>
@endif
</div>
@endforeach
</div>
</div>

View File

@ -64,7 +64,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') }}

View File

@ -238,6 +238,8 @@ Route::middleware('auth')->group(function () {
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::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']);