diff --git a/.env.example b/.env.example index 69691b125..6b3fd21b5 100755 --- a/.env.example +++ b/.env.example @@ -1,7 +1,9 @@ -APP_NAME=Laravel +APP_NAME="Laravel Auth" APP_ENV=local APP_KEY= APP_DEBUG=true +# For XAMPP, set this to http://localhost/laravel-auth/public +# For php artisan serve, use http://localhost:8000 APP_URL=http://localhost:8000 APP_PROJECT_VERSION=12 @@ -10,8 +12,10 @@ LOG_CHANNEL=stack DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 -DB_DATABASE=laravel +# For XAMPP: database name you create in phpMyAdmin +DB_DATABASE=laravel_auth DB_USERNAME=root +# XAMPP default has no password; set one in production DB_PASSWORD= BROADCAST_DRIVER=pusher @@ -25,12 +29,14 @@ REDIS_PASSWORD=null REDIS_PORT=6379 MAIL_MAILER=smtp +# For local dev: use Mailtrap (https://mailtrap.io) or set to 'log' to write emails to storage/logs +# MAIL_MAILER=log MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS=null +MAIL_FROM_ADDRESS="noreply@laravel-auth.test" MAIL_FROM_NAME="${APP_NAME}" EMAIL_EXCEPTION_ENABLED=false diff --git a/INSTALLATION.md b/INSTALLATION.md new file mode 100644 index 000000000..dc5a97e13 --- /dev/null +++ b/INSTALLATION.md @@ -0,0 +1,251 @@ +# Laravel Auth — Installation Guide + +A step-by-step guide for setting up **Laravel Auth** on **XAMPP (localhost)** or any standard LAMP/LEMP server. + +--- + +## Requirements + +| Requirement | Minimum Version | +|---|---| +| PHP | 8.1+ | +| MySQL | 5.7+ / MariaDB 10.4+ | +| Composer | 2.x | +| Node.js + npm | 18.x+ | +| XAMPP | 8.x (with PHP 8.1+) | + +--- + +## Quick Start (XAMPP) + +### 1. Clone the Repository + +```bash +cd C:\xampp\htdocs +git clone https://github.com/jeremykenedy/laravel-auth.git laravel-auth +cd laravel-auth +``` + +### 2. Install PHP Dependencies + +```bash +composer install +``` + +### 3. Install Node Dependencies & Build Assets + +```bash +npm install +npm run dev # development build +# or +npm run build # production build +``` + +### 4. Configure Environment + +```bash +cp .env.example .env +php artisan key:generate +``` + +Edit `.env` and update these values: + +```dotenv +APP_NAME="Laravel Auth" +APP_URL=http://localhost/laravel-auth/public + +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=laravel_auth # create this database in phpMyAdmin first +DB_USERNAME=root +DB_PASSWORD= # leave empty for default XAMPP +``` + +> **Tip:** For email verification during development, set `MAIL_MAILER=log`. +> Emails will be written to `storage/logs/laravel.log` instead of being sent. + +### 5. Create the Database + +Open **phpMyAdmin** (`http://localhost/phpmyadmin`) and create a new database named `laravel_auth` (utf8mb4_unicode_ci collation). + +### 6. Run Migrations & Seed Data + +```bash +php artisan migrate --seed +``` + +This creates all tables and seeds: +- Default roles (admin, user, unverified) +- Default permissions +- A demo admin user +- Theme options + +### 7. Configure Storage Link + +```bash +php artisan storage:link +``` + +### 8. Set Folder Permissions (Linux/Mac only) + +```bash +chmod -R 775 storage bootstrap/cache +``` + +### 9. Access the Application + +| Method | URL | +|---|---| +| XAMPP (Apache) | `http://localhost/laravel-auth/public` | +| Artisan Dev Server | `php artisan serve` → `http://localhost:8000` | + +--- + +## Default Login Credentials + +After seeding, you can login with: + +| Role | Email | Password | +|---|---|---| +| Admin | `admin@admin.com` | `password` | +| User | `user@user.com` | `password` | + +> **Important:** Change these passwords immediately after first login. + +--- + +## Key Features + +- ✅ Email registration with activation +- ✅ Strong password policy (8+ chars, mixed case, number, symbol) +- ✅ Forgot / Reset password +- ✅ Remember Me +- ✅ Logout with session invalidation + confirmation +- ✅ Social authentication (Google, Facebook, Twitter, GitHub, etc.) +- ✅ Two-step verification (optional, off by default) +- ✅ Roles & Permissions system +- ✅ User profile with avatar +- ✅ Admin panel (user management, themes, logs) +- ✅ IP address tracking +- ✅ Laravel Blocker (block IPs / users) +- ✅ reCAPTCHA support + +--- + +## Environment Variables Reference + +### Core Settings + +```dotenv +ACTIVATION=true # Require email activation +ACTIVATION_LIMIT_TIME_PERIOD=24 # Hours before activation link expires +ACTIVATION_LIMIT_MAX_ATTEMPTS=3 # Max activation attempts before lockout +``` + +### Two-Step Auth + +```dotenv +LARAVEL_2STEP_ENABLED=false # Enable two-step verification +``` + +### Social Login (Socialite) + +Configure OAuth credentials from each platform's developer console: + +```dotenv +GOOGLE_ID=your-google-client-id +GOOGLE_SECRET=your-google-secret +GOOGLE_REDIRECT=http://localhost:8000/social/handle/google + +GITHUB_ID=your-github-id +GITHUB_SECRET=your-github-secret +GITHUB_URL=http://localhost:8000/social/handle/github +``` + +### reCAPTCHA + +```dotenv +ENABLE_RECAPTCHA=true +RE_CAP_SITE=your-recaptcha-site-key +RE_CAP_SECRET=your-recaptcha-secret-key +``` + +--- + +## Useful Artisan Commands + +```bash +# Clear all caches +php artisan optimize:clear + +# View all registered routes +php artisan route:list + +# Run tests +php artisan test + +# Reset and re-seed database (⚠️ destroys data) +php artisan migrate:fresh --seed +``` + +--- + +## Password Policy + +Passwords must meet **all** of the following requirements: + +- Minimum **8 characters** +- At least one **uppercase** letter (A–Z) +- At least one **lowercase** letter (a–z) +- At least one **number** (0–9) +- At least one **symbol** (e.g., `!@#$%`) +- Must not be a [known breached password](https://haveibeenpwned.com/Passwords) + +--- + +## Troubleshooting + +**"Class not found" errors** +```bash +composer dump-autoload +``` + +**"No application encryption key" error** +```bash +php artisan key:generate +``` + +**Blank page / 500 error** +Check `storage/logs/laravel.log` for details and ensure `APP_DEBUG=true` in `.env`. + +**Assets not loading** +```bash +npm run build +``` + +**Session/cookie issues** +Ensure `APP_URL` in `.env` exactly matches the URL you're visiting. + +--- + +## Security Hardening for Production + +Before deploying: + +```dotenv +APP_ENV=production +APP_DEBUG=false +``` + +```bash +php artisan config:cache +php artisan route:cache +php artisan view:cache +php artisan optimize +``` + +--- + +## License + +MIT License — see [LICENSE](LICENSE) file for details. diff --git a/README.md b/README.md index efb62c26c..bb6ff8ce1 100755 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ If you like this, you will love [Laravel Auth Spa](https://github.com/jeremykene - [About](#about) - [Features](#features) - [Installation Instructions](#installation-instructions) + - [Full XAMPP / Localhost Guide](INSTALLATION.md) - [Build the Front End Assets with Mix](#build-the-front-end-assets-with-mix) - [Optionally Build Cache](#optionally-build-cache) - [Seeds](#seeds) diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 360da84dd..1f27e8ecb 100755 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -4,9 +4,9 @@ use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesUsers; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Session; class LoginController extends Controller { @@ -28,8 +28,29 @@ class LoginController extends Controller * * @var string */ + protected $redirectTo = '/home'; + + /** + * Where to redirect users after logout. + * + * @var string + */ protected $redirectAfterLogout = '/'; + /** + * Maximum login attempts before lockout. + * + * @var int + */ + protected $maxAttempts = 5; + + /** + * Minutes to lock the user out. + * + * @var int + */ + protected $decayMinutes = 15; + /** * Create a new controller instance. * @@ -41,17 +62,30 @@ public function __construct() } /** - * Logout, Clear Session, and Return. + * Securely log the user out of the application. + * Invalidates the session and regenerates CSRF token. * - * @return void + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse */ - public function logout() + public function logout(Request $request) { - // $user = Auth::user(); - // Log::info('User Logged Out. ', [$user]); + $user = Auth::user(); + + if ($user) { + Log::info('User logged out.', [ + 'user_id' => $user->id, + 'email' => $user->email, + 'ip' => $request->ip(), + ]); + } + Auth::logout(); - Session::flush(); - return redirect(property_exists($this, 'redirectAfterLogout') ? $this->redirectAfterLogout : '/'); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect($this->redirectAfterLogout) + ->with('status', __('You have been successfully logged out.')); } } diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index c1d4cb5fa..05a3ec08f 100755 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -12,6 +12,8 @@ use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; +use Illuminate\Support\Str; +use Illuminate\Validation\Rules\Password; class RegisterController extends Controller { @@ -52,6 +54,13 @@ public function __construct() /** * Get a validator for an incoming registration request. * + * Enforces strong password policy: + * - Minimum 8 characters + * - At least one uppercase and one lowercase letter + * - At least one number + * - At least one symbol + * - Cannot be a commonly-used password (uncompromised check) + * * @param array $data * @return \Illuminate\Contracts\Validation\Validator */ @@ -66,11 +75,19 @@ protected function validator(array $data) return Validator::make( $data, [ - 'name' => 'required|max:255|unique:users|alpha_dash', - 'first_name' => 'alpha_dash', - 'last_name' => 'alpha_dash', - 'email' => 'required|email|max:255|unique:users', - 'password' => 'required|min:6|max:30|confirmed', + 'name' => 'required|string|max:255|unique:users|alpha_dash', + 'first_name' => 'nullable|string|max:255|alpha_dash', + 'last_name' => 'nullable|string|max:255|alpha_dash', + 'email' => 'required|string|email:rfc,dns|max:255|unique:users', + 'password' => [ + 'required', + 'confirmed', + Password::min(8) + ->mixedCase() + ->numbers() + ->symbols() + ->uncompromised(), + ], 'password_confirmation' => 'required|same:password', 'g-recaptcha-response' => '', 'captcha' => 'required|min:1', @@ -78,13 +95,13 @@ protected function validator(array $data) [ 'name.unique' => trans('auth.userNameTaken'), 'name.required' => trans('auth.userNameRequired'), - 'first_name.required' => trans('auth.fNameRequired'), - 'last_name.required' => trans('auth.lNameRequired'), + 'name.alpha_dash' => __('Username may only contain letters, numbers, dashes, and underscores.'), + 'first_name.alpha_dash' => __('First name may only contain letters, numbers, dashes, and underscores.'), + 'last_name.alpha_dash' => __('Last name may only contain letters, numbers, dashes, and underscores.'), 'email.required' => trans('auth.emailRequired'), 'email.email' => trans('auth.emailInvalid'), + 'email.unique' => __('This email address is already registered. Please login or use a different email.'), 'password.required' => trans('auth.passwordRequired'), - 'password.min' => trans('auth.PasswordMin'), - 'password.max' => trans('auth.PasswordMax'), 'g-recaptcha-response.required' => trans('auth.captchaRequire'), 'captcha.min' => trans('auth.CaptchaWrong'), ] @@ -102,20 +119,20 @@ protected function create(array $data) $ipAddress = new CaptureIpTrait(); if (config('settings.activation')) { - $role = Role::where('slug', '=', 'unverified')->first(); + $role = Role::where('slug', '=', 'unverified')->first(); $activated = false; } else { - $role = Role::where('slug', '=', 'user')->first(); + $role = Role::where('slug', '=', 'user')->first(); $activated = true; } $user = User::create([ 'name' => strip_tags($data['name']), - 'first_name' => strip_tags($data['first_name']), - 'last_name' => strip_tags($data['last_name']), - 'email' => $data['email'], + 'first_name' => strip_tags($data['first_name'] ?? ''), + 'last_name' => strip_tags($data['last_name'] ?? ''), + 'email' => strtolower(trim($data['email'])), 'password' => Hash::make($data['password']), - 'token' => str_random(64), + 'token' => Str::random(64), 'signup_ip_address' => $ipAddress->getClientIp(), 'activated' => $activated, ]); diff --git a/app/Http/Requests/Auth/RegisterRequest.php b/app/Http/Requests/Auth/RegisterRequest.php new file mode 100644 index 000000000..0f953eaae --- /dev/null +++ b/app/Http/Requests/Auth/RegisterRequest.php @@ -0,0 +1,80 @@ + + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255', 'unique:users', 'alpha_dash'], + 'first_name' => ['nullable', 'string', 'max:255', 'alpha_dash'], + 'last_name' => ['nullable', 'string', 'max:255', 'alpha_dash'], + 'email' => ['required', 'string', 'email:rfc,dns', 'max:255', 'unique:users'], + 'password' => [ + 'required', + 'confirmed', + Password::min(8) + ->mixedCase() + ->numbers() + ->symbols() + ->uncompromised(), + ], + 'password_confirmation' => ['required', 'same:password'], + ]; + } + + /** + * Custom validation messages. + * + * @return array + */ + public function messages(): array + { + return [ + 'name.required' => __('A username is required.'), + 'name.unique' => __('This username is already taken. Please choose another.'), + 'name.alpha_dash' => __('Username may only contain letters, numbers, dashes, and underscores.'), + 'email.required' => __('An email address is required.'), + 'email.email' => __('Please enter a valid email address.'), + 'email.unique' => __('This email address is already registered. Please login or use a different email.'), + 'password.required' => __('A password is required.'), + 'password.confirmed' => __('The password confirmation does not match.'), + ]; + } + + /** + * Prepare the data for validation — trim and normalise inputs. + */ + protected function prepareForValidation(): void + { + $this->merge([ + 'name' => trim($this->name ?? ''), + 'first_name' => trim($this->first_name ?? ''), + 'last_name' => trim($this->last_name ?? ''), + 'email' => strtolower(trim($this->email ?? '')), + ]); + } +} diff --git a/app/Http/Requests/UpdateUserPasswordRequest.php b/app/Http/Requests/UpdateUserPasswordRequest.php index 4e204841c..94338731b 100644 --- a/app/Http/Requests/UpdateUserPasswordRequest.php +++ b/app/Http/Requests/UpdateUserPasswordRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rules\Password; class UpdateUserPasswordRequest extends FormRequest { @@ -11,7 +12,7 @@ class UpdateUserPasswordRequest extends FormRequest * * @return bool */ - public function authorize() + public function authorize(): bool { return true; } @@ -19,27 +20,34 @@ public function authorize() /** * Get the validation rules that apply to the request. * - * @return array + * @return array */ - public function rules() + public function rules(): array { return [ - 'password' => 'required|min:6|max:20|confirmed', - 'password_confirmation' => 'required|same:password', + 'password' => [ + 'required', + 'confirmed', + Password::min(8) + ->mixedCase() + ->numbers() + ->symbols() + ->uncompromised(), + ], + 'password_confirmation' => ['required', 'same:password'], ]; } /** * Get the error messages for the defined validation rules. * - * @return array + * @return array */ - public function messages() + public function messages(): array { return [ - 'password.required' => trans('auth.passwordRequired'), - 'password.min' => trans('auth.PasswordMin'), - 'password.max' => trans('auth.PasswordMax'), + 'password.required' => trans('auth.passwordRequired'), + 'password.confirmed' => __('The password confirmation does not match.'), ]; } } diff --git a/app/Models/User.php b/app/Models/User.php index daf3582ff..03b0d14b4 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -46,7 +46,6 @@ class User extends Authenticatable protected $hidden = [ 'password', 'remember_token', - 'activated', 'token', ]; @@ -90,6 +89,7 @@ class User extends Authenticatable 'admin_ip_address' => 'string', 'updated_ip_address' => 'string', 'deleted_ip_address' => 'string', + 'email_verified_at' => 'datetime', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', @@ -100,6 +100,27 @@ public function sendPasswordResetNotification($token) $this->notify(new ResetPasswordNotification($token)); } + /** + * Scope a query to only include active (activated) users. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeActive($query) + { + return $query->where('activated', true); + } + + /** + * Get the user's full name. + * + * @return string + */ + public function getFullNameAttribute(): string + { + return trim("{$this->first_name} {$this->last_name}") ?: $this->name; + } + /** * Get the socials for the user. */ diff --git a/database/migrations/2026_02_27_000000_add_performance_indexes_to_users_table.php b/database/migrations/2026_02_27_000000_add_performance_indexes_to_users_table.php new file mode 100644 index 000000000..950adbd8b --- /dev/null +++ b/database/migrations/2026_02_27_000000_add_performance_indexes_to_users_table.php @@ -0,0 +1,67 @@ +indexExists('users', 'users_activated_index')) { + $table->index('activated', 'users_activated_index'); + } + + // Index on deleted_at for soft delete queries + if (! $this->indexExists('users', 'users_deleted_at_index')) { + $table->index('deleted_at', 'users_deleted_at_index'); + } + + // Composite index for activated + deleted_at — most common combined filter + if (! $this->indexExists('users', 'users_activated_deleted_at_index')) { + $table->index(['activated', 'deleted_at'], 'users_activated_deleted_at_index'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropIndexIfExists('users_activated_index'); + $table->dropIndexIfExists('users_deleted_at_index'); + $table->dropIndexIfExists('users_activated_deleted_at_index'); + }); + } + + /** + * Check whether a given index already exists on the table. + * + * @param string $table + * @param string $indexName + * @return bool + */ + private function indexExists(string $table, string $indexName): bool + { + $conn = Schema::getConnection(); + $dbName = $conn->getDatabaseName(); + $indexes = $conn->select( + "SELECT INDEX_NAME FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME = ?", + [$dbName, $table, $indexName] + ); + + return count($indexes) > 0; + } +}; diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 5ff1762fb..6527ae212 100755 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -1,76 +1,234 @@ @extends('layouts.app') +@section('template_title') + {{ __('Login') }} +@endsection + +@section('template_fastload_css') + .auth-card-wrapper { + min-height: calc(100vh - 120px); + display: flex; + align-items: center; + } + .auth-card { + border: none; + border-radius: 12px; + box-shadow: 0 4px 24px rgba(0,0,0,.10); + } + .auth-card .card-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #fff; + border-radius: 12px 12px 0 0 !important; + padding: 1.5rem 2rem; + border-bottom: none; + } + .auth-card .card-header h4 { + margin: 0; + font-weight: 600; + letter-spacing: .5px; + } + .auth-card .card-body { + padding: 2rem; + } + .btn-auth { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + padding: .6rem 2rem; + font-weight: 600; + letter-spacing: .5px; + transition: opacity .2s; + } + .btn-auth:hover { opacity: .88; } + .password-wrapper { position: relative; } + .password-toggle { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + cursor: pointer; + color: #aaa; + border: none; + background: none; + padding: 0 4px; + z-index: 5; + } + .password-toggle:hover { color: #555; } + .divider-text { + position: relative; + text-align: center; + margin: 1.25rem 0; + color: #999; + font-size: .85rem; + } + .divider-text::before, + .divider-text::after { + content: ''; + position: absolute; + top: 50%; + width: 42%; + height: 1px; + background: #e2e8f0; + } + .divider-text::before { left: 0; } + .divider-text::after { right: 0; } +@endsection + @section('content') -
-
-
-
-
{{ __('Login') }}
+
+
+
-
-
- @csrf + {{-- Flash Messages --}} + @if (session('status')) + + @endif -
- +
+
+

{{ __('Sign In') }}

+ {{ __('Welcome back! Please sign in to continue.') }} +
-
- +
+ + @csrf + {{-- Email --}} +
+ +
+
+ + + +
+ @if ($errors->has('email')) - +
+ {{ $errors->first('email') }} - +
@endif
-
- - -
- - + {{-- Password --}} +
+
+ + + {{ __('Forgot password?') }} + +
+
+
+ + + +
+ + @if ($errors->has('password')) - +
+ {{ $errors->first('password') }} - +
@endif
-
-
-
- -
+ {{-- Remember Me --}} +
+
+ +
-
-
- + {{-- Submit --}} + - - {{ __('auth.forgot') }} - -
-
+ @if (config('settings.reCaptchStatus')) +
+ @endif -

- Or Login with -

+
{{ __('or continue with') }}
@include('partials.socials-icons')
+ +
+
@endsection + +@section('footer_scripts') + @if (config('settings.reCaptchStatus')) + + @endif + +@endsection diff --git a/resources/views/auth/passwords/email.blade.php b/resources/views/auth/passwords/email.blade.php index 4d4690073..c0d2ed48c 100755 --- a/resources/views/auth/passwords/email.blade.php +++ b/resources/views/auth/passwords/email.blade.php @@ -1,46 +1,93 @@ @extends('layouts.app') +@section('template_title') + {{ __('Forgot Password') }} +@endsection + +@section('template_fastload_css') + .auth-card-wrapper { min-height: calc(100vh - 120px); display: flex; align-items: center; } + .auth-card { border: none; border-radius: 12px; box-shadow: 0 4px 24px rgba(0,0,0,.10); } + .auth-card .card-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #fff; + border-radius: 12px 12px 0 0 !important; + padding: 1.5rem 2rem; + border-bottom: none; + } + .auth-card .card-header h4 { margin: 0; font-weight: 600; } + .auth-card .card-body { padding: 2rem; } + .btn-auth { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; padding: .6rem 2rem; font-weight: 600; + } + .font-weight-600 { font-weight: 600; } +@endsection + @section('content') -
-
-
-
-
{{ __('Reset Password') }}
+
+
+
+ +
+
+

{{ __('Forgot Password') }}

+ {{ __("Enter your email and we'll send you a reset link.") }} +
@if (session('status')) -
- {{ session('status') }} + @endif -
+ @csrf -
- - -
- - +
+ +
+
+ + + +
+ @if ($errors->has('email')) - +
+ {{ $errors->first('email') }} - +
@endif
-
-
- -
-
+
+ +
+
diff --git a/resources/views/auth/passwords/reset.blade.php b/resources/views/auth/passwords/reset.blade.php index 82ea58bb6..4af533687 100755 --- a/resources/views/auth/passwords/reset.blade.php +++ b/resources/views/auth/passwords/reset.blade.php @@ -1,75 +1,226 @@ @extends('layouts.app') +@section('template_title') + {{ __('Reset Password') }} +@endsection + +@section('template_fastload_css') + .auth-card-wrapper { min-height: calc(100vh - 120px); display: flex; align-items: center; } + .auth-card { border: none; border-radius: 12px; box-shadow: 0 4px 24px rgba(0,0,0,.10); } + .auth-card .card-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #fff; + border-radius: 12px 12px 0 0 !important; + padding: 1.5rem 2rem; + border-bottom: none; + } + .auth-card .card-header h4 { margin: 0; font-weight: 600; } + .auth-card .card-body { padding: 2rem; } + .btn-auth { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; padding: .6rem 2rem; font-weight: 600; + } + .password-wrapper { position: relative; } + .password-toggle { + position: absolute; right: 10px; top: 50%; transform: translateY(-50%); + cursor: pointer; color: #aaa; border: none; background: none; padding: 0 4px; z-index: 5; + } + /* Strength bar */ + .req-item { font-size: .78rem; transition: color .2s; } + .req-item.met { color: #28a745; } + .req-item.unmet{ color: #bbb; } + .font-weight-600 { font-weight: 600; } +@endsection + @section('content') -
-
-
-
-
{{ __('Reset Password') }}
+
+
+
+ +
+
+

{{ __('Set New Password') }}

+ {{ __('Choose a strong password to protect your account.') }} +
@if (session('status')) -
- {{ session('status') }} + @endif -
+ + @csrf -
- - -
- - + {{-- Email --}} +
+ +
+
+ + + +
+ @if ($errors->has('email')) - +
+ {{ $errors->first('email') }} - +
@endif
-
- - -
- - + {{-- New Password --}} +
+ +
+
+ + + +
+ + @if ($errors->has('password')) - +
+ {{ $errors->first('password') }} - +
@endif
-
- -
- + {{-- Strength indicators --}} +
+
+
+
+
+ 8+ chars + Uppercase + Lowercase + Number + Symbol +
+
+ {{-- Confirm Password --}} +
+ +
+
+ + + +
+ + @if ($errors->has('password_confirmation')) - +
{{ $errors->first('password_confirmation') }} - +
@endif
-
-
- -
-
+
+
@endsection + +@section('footer_scripts') + +@endsection diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index f9f66a40b..fd5c42e06 100755 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -1,130 +1,352 @@ @extends('layouts.app') +@section('template_title') + {{ __('Create Account') }} +@endsection + +@section('template_fastload_css') + .auth-card-wrapper { + min-height: calc(100vh - 120px); + display: flex; + align-items: flex-start; + padding-top: 2rem; + padding-bottom: 2rem; + } + .auth-card { + border: none; + border-radius: 12px; + box-shadow: 0 4px 24px rgba(0,0,0,.10); + } + .auth-card .card-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #fff; + border-radius: 12px 12px 0 0 !important; + padding: 1.5rem 2rem; + border-bottom: none; + } + .auth-card .card-header h4 { margin: 0; font-weight: 600; letter-spacing: .5px; } + .auth-card .card-body { padding: 2rem; } + .btn-auth { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + padding: .6rem 2rem; + font-weight: 600; + letter-spacing: .5px; + transition: opacity .2s; + } + .btn-auth:hover { opacity: .88; } + .password-wrapper { position: relative; } + .password-toggle { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + cursor: pointer; + color: #aaa; + border: none; + background: none; + padding: 0 4px; + z-index: 5; + } + .password-toggle:hover { color: #555; } + /* Password strength bar */ + .strength-bar { height: 5px; border-radius: 3px; transition: width .3s, background .3s; } + .req-item { font-size: .78rem; transition: color .2s; } + .req-item.met { color: #28a745; } + .req-item.unmet{ color: #bbb; } + .divider-text { + position: relative; text-align: center; margin: 1.25rem 0; color: #999; font-size: .85rem; + } + .divider-text::before, .divider-text::after { + content: ''; position: absolute; top: 50%; width: 40%; height: 1px; background: #e2e8f0; + } + .divider-text::before { left: 0; } + .divider-text::after { right: 0; } + .font-weight-600 { font-weight: 600; } +@endsection + @section('content') -
-
-
-
-
{{ __('Register') }}
+
+
+
+ +
+
+

{{ __('Create Account') }}

+ {{ __('Fill in the form below to get started.') }} +
-
+ @csrf -
- - -
- - + {{-- Username --}} +
+ +
+
+ + + +
+ @if ($errors->has('name')) - +
+ {{ $errors->first('name') }} - +
@endif
+ {{ __('Letters, numbers, dashes and underscores only.') }}
-
- - -
- - + {{-- First & Last Name --}} +
+
+ + @if ($errors->has('first_name')) - +
{{ $errors->first('first_name') }} - +
@endif
-
- -
- - -
- - +
+ + @if ($errors->has('last_name')) - +
{{ $errors->first('last_name') }} - +
@endif
-
- - -
- - + {{-- Email --}} +
+ +
+
+ + + +
+ @if ($errors->has('email')) - +
+ {{ $errors->first('email') }} - +
@endif
-
- - -
- - + {{-- Password --}} +
+ +
+
+ + + +
+ + @if ($errors->has('password')) - +
+ {{ $errors->first('password') }} - +
@endif
-
- - -
- + {{-- Strength Bar --}} +
+
+
+
+
+ {{ __('8+ chars') }} + {{ __('Uppercase') }} + {{ __('Lowercase') }} + {{ __('Number') }} + {{ __('Symbol') }}
- - @if(config('settings.reCaptchStatus')) -
-
-
+ {{-- Confirm Password --}} +
+ +
+
+ + +
-
- @endif - -
-
- + @if ($errors->has('password_confirmation')) +
+ {{ $errors->first('password_confirmation') }} +
+ @endif
+ {{-- reCAPTCHA --}} + @if (config('settings.reCaptchStatus')) +
+
+
+ @endif + + {{-- Submit --}} + + +
{{ __('or register with') }}
+
-
-

- Or Use Social Logins to Register -

+
@include('partials.socials')
+ +
+
@endsection @section('footer_scripts') - @if(config('settings.reCaptchStatus')) - + @if (config('settings.reCaptchStatus')) + @endif + @endsection diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 44503769e..a89276ce7 100755 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -5,9 +5,12 @@ + {{-- Security headers via meta --}} + + @hasSection('template_title')@yield('template_title') | @endif {{ config('app.name', Lang::get('titles.app')) }} - - + + @@ -61,5 +64,28 @@ @endif @yield('footer_scripts') + diff --git a/resources/views/partials/nav.blade.php b/resources/views/partials/nav.blade.php index 1a3992ecd..eef988e4c 100755 --- a/resources/views/partials/nav.blade.php +++ b/resources/views/partials/nav.blade.php @@ -84,13 +84,11 @@ {!! trans('titles.profile') !!} - - {{ __('Logout') }} - -