Files
timebank-cc-public/references/PROFILE_DIRECT_LOGIN.md
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

16 KiB

Profile Direct Login Feature

Overview

This feature allows you to create direct links to User, Organization, Bank, and Admin profile logins that can be used in emails or other external communications. The system handles the authentication flow automatically for all profile types, with automatic username pre-filling for better user experience.

How It Works

The direct login route implements a secure, multi-step authentication flow:

  1. User Authentication Check

    • If the user is not logged in with their personal User profile, they are redirected to the user login page
    • For User profiles: Username is automatically pre-filled on the login form via URL parameter
    • After successful user login, they are automatically redirected back to continue the profile switch flow
  2. Relationship Verification

    • The system verifies that the authenticated user has access to the specified profile:
      • User: Authenticated user must match the target user
      • Organization: User owns/is a member of the organization
      • Bank: User manages the bank
      • Admin: User has this admin profile
    • If the user doesn't have access, a 403 Forbidden error is returned
  3. Profile Switch

    • User: Direct redirect to main page (or custom intended URL)
    • Organization: Direct switch without password (matches normal profile switching behavior)
    • Bank: Redirected to bank password entry page
    • Admin: Redirected to admin password entry page
  4. Final Redirect

    • User: Redirected to intended URL or main page
    • Organization/Bank/Admin: Redirected to intended URL or main page after profile switch

Usage

User Profile

To create a link to a user login:

route('user.direct-login', ['userId' => $user->id])

Example URL:

https://yoursite.com/user/123/login

With custom intended destination (e.g., profile edit) and username pre-fill:

route('user.direct-login', [
    'userId' => $user->id,
    'intended' => route('profile.edit'),
    'name' => $user->name  // Optional: pre-fills username on login form
])

Example URL with username pre-fill:

https://yoursite.com/nl/user/123/login?intended=https%3A%2F%2Fyoursite.com%2Fnl%2Fprofiel%2Fbewerken&name=johndoe

Note:

  • User direct login defaults to redirecting to the main page after login, but you can override this with the intended parameter
  • The name parameter is automatically included when generating links via ProfileEditedByAdminMail for improved user experience
  • When the user is redirected to the login page, the username field will be automatically pre-filled with the value from the name parameter

Organization Profile

To create a link to an organization login:

route('organization.direct-login', ['organizationId' => $organization->id])

Example URL:

https://yoursite.com/organization/123/login

With intended destination:

route('organization.direct-login', [
    'organizationId' => $organization->id,
    'intended' => route('organization.settings')
])

Bank Profile

To create a link to a bank login:

route('bank.direct-login', ['bankId' => $bank->id])

Example URL:

https://yoursite.com/bank/456/login

With intended destination:

route('bank.direct-login', [
    'bankId' => $bank->id,
    'intended' => route('transactions.review')
])

Admin Profile

To create a link to an admin login:

route('admin.direct-login', ['adminId' => $admin->id])

Example URL:

https://yoursite.com/admin/789/login

With intended destination:

route('admin.direct-login', [
    'adminId' => $admin->id,
    'intended' => route('admin.dashboard')
])

In Email Templates

<a href="{{ route('organization.direct-login', ['organizationId' => $organization->id]) }}">
    Login to {{ $organization->name }}
</a>

Or with a specific destination:

<a href="{{ route('organization.direct-login', ['organizationId' => $organization->id, 'intended' => route('post.edit', $post->id)]) }}">
    Edit this post as {{ $organization->name }}
</a>

Security Features

Multi-Layer Authentication

  • User Guard First: Ensures base authentication is established
  • Relationship Verification: Only users with proper access can switch to the organization
  • Password Re-Authentication: Separate password required for organization access

Session Management

  • Profile switch intent is stored in encrypted session
  • Intended URLs are validated and sanitized
  • Sessions are cleared after successful authentication

Access Control

  • 404 error if organization doesn't exist
  • 403 error if user doesn't have access to organization
  • All authentication follows the existing SwitchGuardTrait security pattern

Flow Diagram

User clicks email link
         |
         v
Is user authenticated? --NO--> User Login --> [Back to this flow]
         |
        YES
         v
Does user own/manage org? --NO--> 403 Forbidden
         |
        YES
         v
Set profile switch intent
         |
         v
Organization password page
         |
         v
Password correct? --NO--> Error message
         |
        YES
         v
Switch to Organization guard
         |
         v
Redirect to intended URL or main page

Implementation Details

Route Definitions

Located in routes/web.php:

User Route (Guest accessible - no auth middleware):

// Located in the guest routes section (line ~182)
// Accessible to both authenticated and non-authenticated users
Route::get('/user/{userId}/login', [UserLoginController::class, 'directLogin'])
    ->name('user.direct-login');

Organization/Bank/Admin Routes (Inside auth middleware group):

// Located inside the auth middleware group (line ~439+)
// Require user authentication before accessing
Route::get('/organization/{organizationId}/login', [OrganizationLoginController::class, 'directLogin'])
    ->name('organization.direct-login');

Route::get('/bank/{bankId}/login', [BankLoginController::class, 'directLogin'])
    ->name('bank.direct-login');

Route::get('/admin/{adminId}/login', [AdminLoginController::class, 'directLogin'])
    ->name('admin.direct-login');

Important: The user route is placed in the guest routes section (outside auth middleware) to allow unauthenticated users to access it. The controller handles authentication checks internally and redirects to login when needed.

Controller Methods

Located in:

  • app/Http/Controllers/UserLoginController.php
  • app/Http/Controllers/OrganizationLoginController.php
  • app/Http/Controllers/BankLoginController.php
  • app/Http/Controllers/AdminLoginController.php

Each directLogin() method handles:

  • Profile existence validation
  • User authentication check
  • Relationship verification:
    • User: Authenticated user must match target user
    • Organization: User owns/is a member of the organization
    • Bank: User manages the bank
    • Admin: User has this admin profile
  • Session intent setting (for Organization, Bank, Admin)
  • Proper redirects for each step

User-specific features:

  • Username Pre-fill: When redirecting to login, the name query parameter is preserved and passed to the login page
  • Localized URLs: Uses LaravelLocalization to generate properly localized redirect URLs
  • URL Parameter Handling: The name parameter from the direct login URL is forwarded to the login page as a query parameter
  • The login form reads the name parameter via request()->input('name') and pre-fills the username field

Login Form Implementation

The login form in resources/views/auth/login.blade.php supports username pre-filling via URL parameter:

<form method="POST" action="{{ route('login') }}">
    @csrf

    <div>
        <x-jetstream.label for="name" value="{!! __('Email or username') !!}" />
        <x-jetstream.input
            id="name"
            class="block mt-1 w-full"
            type="text"
            name="name"
            value="{{ old('name', request()->input('name')) }}"
            required
            autofocus
            autocomplete="username"
        />
    </div>

    <!-- Password field... -->
</form>

Key points:

  • The value attribute uses old('name', request()->input('name')) to:
    1. First check for validation errors (old input)
    2. Fall back to the URL query parameter if no old input exists
  • The autocomplete="username" attribute helps browsers identify the field correctly
  • This works for direct URL access (e.g., /nl/inloggen?name=johndoe) as well as redirects

Session Keys Used

For all profile types:

  • url.intended: Laravel's standard intended redirect URL (used for user login redirect back to profile login)
  • intended_profile_switch_type: Set to 'Organization', 'Bank', or 'Admin'
  • intended_profile_switch_id: Profile ID

Profile-specific:

  • organization_login_intended_url: Optional final destination URL after organization login
  • bank_login_intended_url: Optional final destination URL after bank login
  • admin_login_intended_url: Optional final destination URL after admin login

Examples

// User
$url = route('user.direct-login', ['userId' => 123]);
// Result: https://yoursite.com/user/123/login

// Organization
$url = route('organization.direct-login', ['organizationId' => 5]);
// Result: https://yoursite.com/organization/5/login

// Bank
$url = route('bank.direct-login', ['bankId' => 2]);
// Result: https://yoursite.com/bank/2/login

// Admin
$url = route('admin.direct-login', ['adminId' => 1]);
// Result: https://yoursite.com/admin/1/login
// User: Direct link to profile edit page
$url = route('user.direct-login', [
    'userId' => $user->id,
    'intended' => route('profile.edit')
]);

// Organization: Direct link to post management page
$url = route('organization.direct-login', [
    'organizationId' => $org->id,
    'intended' => route('posts.index')
]);

// Bank: Direct link to transaction review
$url = route('bank.direct-login', [
    'bankId' => $bank->id,
    'intended' => route('transactions.pending')
]);

// Admin: Direct link to user management
$url = route('admin.direct-login', [
    'adminId' => $admin->id,
    'intended' => route('admin.users')
]);

Example 3: In Blade Email Templates

User Profile Edited Notification (Automated via ProfileEditedByAdminMail):

The ProfileEditedByAdminMail class automatically generates properly localized URLs with username pre-fill:

// In ProfileEditedByAdminMail.php constructor:
$profileEditPath = LaravelLocalization::getURLFromRouteNameTranslated(
    $this->locale,
    'routes.profile.edit'
);
$profileEditUrl = url($profileEditPath);

// Direct user login with redirect to profile.edit and username pre-filled
$this->buttonUrl = LaravelLocalization::localizeURL(
    route('user.direct-login', [
        'userId' => $profile->id,
        'intended' => $profileEditUrl,
        'name' => $profile->name  // Username pre-fill
    ]),
    $this->locale
);

Generated URL example:

https://yoursite.com/nl/user/2/login?intended=https%3A%2F%2Fyoursite.com%2Fnl%2Fprofiel%2Fbewerken&name=johndoe

Email template usage:

@component('emails.layouts.html', ['locale' => $locale])
    <p>Hello {{ $profile->full_name ?? $profile->name }},</p>
    <p>An administrator has made changes to your profile.</p>

    <table role="presentation" class="mobile-button" cellpadding="0" cellspacing="0" border="0" align="center">
        <tr>
            <td align="center" style="background-color: {{ theme_color('brand') }};">
                <a href="{{ $buttonUrl }}" style="color: #ffffff;">
                    Review Your Profile
                </a>
            </td>
        </tr>
    </table>
@endcomponent

Organization Event Review:

@component('emails.layouts.html', ['locale' => 'en'])
    <p>Hello {{ $user->name }},</p>
    <p>A new event requires your organization's approval.</p>

    <table role="presentation" class="mobile-button" cellpadding="0" cellspacing="0" border="0" align="center">
        <tr>
            <td align="center" style="background-color: {{ theme_color('brand') }};">
                <a href="{{ route('organization.direct-login', [
                    'organizationId' => $organization->id,
                    'intended' => route('event.review', $event->id)
                ]) }}" style="color: #ffffff;">
                    Review Event as {{ $organization->name }}
                </a>
            </td>
        </tr>
    </table>
@endcomponent

Bank Transaction Alert:

@component('emails.layouts.html', ['locale' => 'en'])
    <p>Hello {{ $user->name }},</p>
    <p>A large transaction requires bank approval.</p>

    <table role="presentation" class="mobile-button" cellpadding="0" cellspacing="0" border="0" align="center">
        <tr>
            <td align="center" style="background-color: {{ theme_color('brand') }};">
                <a href="{{ route('bank.direct-login', [
                    'bankId' => $bank->id,
                    'intended' => route('transaction.show', $transaction->id)
                ]) }}" style="color: #ffffff;">
                    Review Transaction as {{ $bank->name }}
                </a>
            </td>
        </tr>
    </table>
@endcomponent

Admin User Report:

@component('emails.layouts.html', ['locale' => 'en'])
    <p>Hello {{ $user->name }},</p>
    <p>A user has been reported and requires admin review.</p>

    <table role="presentation" class="mobile-button" cellpadding="0" cellspacing="0" border="0" align="center">
        <tr>
            <td align="center" style="background-color: {{ theme_color('brand') }};">
                <a href="{{ route('admin.direct-login', [
                    'adminId' => $admin->id,
                    'intended' => route('admin.reports.show', $report->id)
                ]) }}" style="color: #ffffff;">
                    Review Report as Admin
                </a>
            </td>
        </tr>
    </table>
@endcomponent

Troubleshooting

User Gets 404 Error

  • The profile ID doesn't exist
  • Check that the profile hasn't been soft-deleted
  • Verify correct profile type is being used

User Gets 403 Error

  • User doesn't have access to the profile
  • For User: Authenticated user ID doesn't match target user ID
  • For Organization: Verify relationship exists in organization_user table
  • For Bank: Verify user is listed as manager in bank_managers table
  • For Admin: Verify user has this admin profile in admin_user table

Redirect Loop

  • Ensure user login is working correctly
  • Check that session storage is configured properly
  • Verify middleware chain is correct

Username Not Pre-filled on Login Form

  • Check that the name parameter is included in the direct login URL
  • Verify the login form is using request()->input('name') to read the parameter
  • Check browser developer tools to confirm the query parameter is present in the URL
  • Ensure the login view has: value="{{ old('name', request()->input('name')) }}"

User Direct Login Redirects to Login Without Query Parameters

Problem: Query parameters (intended and name) are stripped during redirect

Cause: The user.direct-login route is inside the auth middleware group, which redirects unauthenticated users to login before the controller can handle the parameters

Solution: Ensure the user.direct-login route is defined in the guest routes section of routes/web.php (around line 182), NOT inside the auth middleware group. The route should only have localization middleware, not authentication middleware:

// ✅ Correct - in guest routes section
Route::get('/user/{userId}/login', [UserLoginController::class, 'directLogin'])
    ->name('user.direct-login');

// ❌ Wrong - inside auth middleware group
Route::middleware(['auth:web'])->group(function () {
    Route::get('/user/{userId}/login', [...])  // Don't place here!
});

Verify with: php artisan route:list --name=user.direct-login -v

The route should show only these middleware:

  • web
  • LocaleSessionRedirect
  • LaravelLocalizationRedirectFilter
  • LaravelLocalizationViewPath

If you see Authenticate:web or other auth middleware, the route is in the wrong location.

  • SECURITY_OVERVIEW.md - Complete authentication system documentation
  • Multi-Guard Authentication System
  • Profile Switch Flow
  • Session Security