]> BookStack Code Mirror - bookstack/commitdiff
Users: Built out auth page for my-account section
authorDan Brown <redacted>
Tue, 17 Oct 2023 16:38:07 +0000 (17:38 +0100)
committerDan Brown <redacted>
Tue, 17 Oct 2023 16:38:07 +0000 (17:38 +0100)
12 files changed:
app/Access/SocialAuthService.php
app/Users/Controllers/UserAccountController.php
lang/en/preferences.php
lang/en/settings.php
resources/icons/security.svg [new file with mode: 0644]
resources/views/settings/layout.blade.php
resources/views/users/account/auth.blade.php [new file with mode: 0644]
resources/views/users/account/layout.blade.php
resources/views/users/api-tokens/parts/list.blade.php
resources/views/users/edit.blade.php
resources/views/users/parts/form.blade.php
routes/web.php

index 24a04ef7e0b4147cd0587113f6e6741100d2ea77..fe919543094f1d636a4b934f5fd2c6674531fe33 100644 (file)
@@ -214,6 +214,7 @@ class SocialAuthService
 
     /**
      * Gets the names of the active social drivers.
+     * @returns array<string, string>
      */
     public function getActiveDrivers(): array
     {
index 9152eb5e822d6f3708190049281d992646d8cb4b..3dd13b85141c0ed697f84ec29e2a3eb6aeda36fe 100644 (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');
+    }
 }
index cf1ee2b37d6bcc807110289a1b2cee2d5115163d..d112b9ebb23b9df8fe4c71acd5eea043f6a0b569 100644 (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.',
 ];
index 9f60606ac57f0506e7af46fc30c7f6d11c6ebda5..579c4b5c856f17ec558c7c8149c78a619524e48b 100644 (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',
diff --git a/resources/icons/security.svg b/resources/icons/security.svg
new file mode 100644 (file)
index 0000000..4fc0d20
--- /dev/null
@@ -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>
\ No newline at end of file
index 7c6f8b002643a580fcc417d1dc172122a083755e..94a8f7725d9ae1acb7c56c06251401877b996052 100644 (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>
diff --git a/resources/views/users/account/auth.blade.php b/resources/views/users/account/auth.blade.php
new file mode 100644 (file)
index 0000000..3503978
--- /dev/null
@@ -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
index 9eaa1eca129977b6bdd703ba63ca33529ad990f7..ff5ad36223a54ffae6e469ce0a4138694d6ca1a8 100644 (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>
index 58617fb85d2f5871e8ce5f3205b69d5703624387..3081682a4ae12d3f6c07733ad51caab2abede4b5 100644 (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)
index 83218693027e0b38acf6c3e01ad978bd18bece29..e6b477a120f0946aaa272045e8c246075679e069 100644 (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)
 
         </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">Connected</p>
+                                @else
+                                    <p class="text-neg bold text-small my-none">Disconnected</p>
+                                @endif
                             </div>
                         @endforeach
                     </div>
index 7ff48a83dd881b9a96f6c1c9f0438dddc844ba40..d9f9588377621539b6ca4e7e8717c7fb57ff743e 100644 (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') }}
index df845bfbcaf1e148f424d970e1f5373e2aa40c6a..a7d3534bd24135273889036e170b8b01101a683d 100644 (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']);