feat: add user authentication

This commit is contained in:
2026-08-19 08:04:02 +03:30
parent a10bdd20fa
commit 84f095b00f
8 changed files with 1548 additions and 8 deletions
@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
/**
* نمایش صفحه ورود
*/
public function index()
{
return view('auth.login');
}
/**
* ورود کاربر
*/
public function post(Request $request)
{
// $credentials = $request->validate([
// 'email' => [
// 'required',
// 'email',
// ],
//
// 'password' => [
// 'required',
// 'string',
// ],
// ],
//
// [
// 'email.required' => 'ایمیل را وارد کنید.',
// 'email.email' => 'فرمت ایمیل صحیح نیست.',
// 'password.required' => 'رمز عبور را وارد کنید.',
// ]);
$remember = $request->boolean('remember');
if (Auth::guard('web')->attempt($credentials, $remember)) {
$request->session()->regenerate();
return redirect()->intended(
route('index')
);
}
return back()
->withInput($request->only('email', 'remember'))
->withErrors([
'email' => 'ایمیل یا رمز عبور اشتباه است.',
]);
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class LogoutController extends Controller
{
//
}
@@ -0,0 +1,107 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\RegisterPostRequest;
use App\Models\City;
use App\Models\Province;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
class RegisterController extends Controller
{
/**
* نمایش فرم ثبت نام
*/
public function index()
{
$provinces = Province::orderBy('name')->get();
return view('auth.register', compact('provinces'));
}
public function post(RegisterPostRequest $request)
{
$imagePath = null;
if ($request->hasFile('image')) {
$imagePath = $request
->file('image')
->store('users', 'public');
}
$user = User::create([
'first_name' => $request->input('first_name'),
'last_name' => $request->input('last_name'),
'national_code' => $request->input('national_code'),
'gender' => $request->input('gender'),
'marital_status' => $request->input('marital_status'),
'spouse_name' => $request->input('marital_status') == 2
? $request->input('spouse_name')
: null,
'mobile' => $request->input('mobile'),
'email' => $request->input('email'),
'username' => $request->input('username'),
'password' => Hash::make($request->input('password')),
'image' => $imagePath,
'province_id' => $request->input('province_id'),
'city_id' => $request->input('city_id'),
'status' => 1,
]);
/*
|--------------------------------------------------------------------------
| ورود خودکار بعد از ثبت نام
|--------------------------------------------------------------------------
*/
Auth::guard('web')->login($user);
$request->session()->regenerate();
/*
|--------------------------------------------------------------------------
| انتقال به پنل کاربری
|--------------------------------------------------------------------------
*/
return redirect()
->route('account.index')
->with('success', 'ثبت‌نام شما با موفقیت انجام شد.');
}
public function cities(Province $province)
{
$cities = City::where('province_id', $province->id)
->orderBy('name')
->get(['id', 'name']);
return response()->json($cities);
}
}
@@ -1,13 +1,11 @@
<?php <?php
namespace App\Http\Requests; namespace App\Http\Requests\Auth;
use App\Enums\PaymentStatus;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PaymentUpdateRequest extends FormRequest class LoginPostRequest extends FormRequest
{ {
/** /**
* Determine if the user is authorized to make this request. * Determine if the user is authorized to make this request.
@@ -25,12 +23,18 @@ class PaymentUpdateRequest extends FormRequest
public function rules(): array public function rules(): array
{ {
return [ return [
'status' => [ 'email' => [
'required', 'required',
'email',
// مقدار باید یکی از مقادیر integer enum باشد (0,1,2,3)
Rule::enum(PaymentStatus::class),
], ],
'password' => [
'required',
'string',
],
]; ];
} }
} }
@@ -0,0 +1,131 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class RegisterPostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'first_name' => [
'required',
'string',
'max:100',
'regex:/^[\x{0600}-\x{06FF}\x{200C}\s]+$/u',
],
'last_name' => [
'required',
'string',
'max:100',
'regex:/^[\x{0600}-\x{06FF}\x{200C}\s]+$/u',
],
'national_code' => [
'required',
'digits:10',
'unique:users,national_code',
],
'gender' => [
'required',
'integer',
Rule::in([1, 2]),
],
'marital_status' => [
'required',
'integer',
Rule::in([1, 2, 3]),
],
'spouse_name' => [
'nullable',
'string',
'max:200',
'required_if:marital_status,2',
'regex:/^[\x{0600}-\x{06FF}\x{200C}\s]+$/u',
],
'mobile' => [
'required',
'digits:11',
'regex:/^09\d{9}$/',
'unique:users,mobile',
],
'email' => [
'required',
'email',
'max:150',
'unique:users,email',
],
'username' => [
'required',
'string',
'min:3',
'max:50',
'regex:/^[A-Za-z0-9]+$/',
'unique:users,username',
],
'password' => [
'required',
'string',
'min:8',
'confirmed',
],
'image' => [
'nullable',
'image',
'mimes:jpg,jpeg,png,webp',
'max:800',
],
'province_id' => [
'nullable',
'integer',
],
'city_id' => [
'nullable',
'integer',
],
];
}
}
+379
View File
@@ -0,0 +1,379 @@
/* =========================================
AUTH PAGE
========================================= */
.auth-page {
min-height: 80vh;
padding: 70px 0;
background: #f5f7fb;
direction: rtl;
}
/* =========================================
CARD
========================================= */
.auth-card {
width: 100%;
max-width: 900px;
margin: 0 auto;
background: #fff;
border-radius: 18px;
padding: 40px;
box-shadow:
0 15px 45px rgba(0, 0, 0, 0.10);
}
.login-card {
max-width: 500px;
}
/* =========================================
HEADER
========================================= */
.auth-header {
text-align: center;
margin-bottom: 35px;
}
.auth-icon {
width: 70px;
height: 70px;
margin: 0 auto 18px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #eef5ff;
color: #1473e6;
font-size: 28px;
}
.auth-header h1 {
margin: 0 0 10px;
font-size: 27px;
font-weight: 700;
color: #222;
}
.auth-header p {
margin: 0;
color: #777;
font-size: 14px;
line-height: 1.8;
}
/* =========================================
FORM SECTION
========================================= */
.form-section {
margin-bottom: 30px;
padding-bottom: 25px;
border-bottom: 1px solid #eee;
}
.form-section:last-of-type {
border-bottom: none;
}
.form-section-title {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 22px;
font-size: 17px;
font-weight: 700;
color: #333;
}
.form-section-title i {
color: #1473e6;
font-size: 19px;
}
/* =========================================
FORM
========================================= */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: #333;
}
.form-group label span {
color: #e53935;
}
.form-control {
width: 100%;
height: 48px;
border: 1px solid #ddd;
border-radius: 9px;
padding: 0 14px;
background: #fff;
color: #333;
font-size: 14px;
transition: all 0.2s ease;
box-shadow: none;
}
.form-control:focus {
border-color: #1473e6;
outline: none;
box-shadow:
0 0 0 3px rgba(20, 115, 230, 0.10);
}
textarea.form-control {
height: auto;
padding-top: 12px;
}
select.form-control {
cursor: pointer;
}
.form-help {
display: block;
margin-top: 6px;
color: #888;
font-size: 12px;
line-height: 1.7;
}
/* =========================================
ALERT
========================================= */
.auth-alert {
display: flex;
gap: 12px;
padding: 15px;
margin-bottom: 25px;
border-radius: 10px;
font-size: 14px;
line-height: 1.8;
}
.auth-alert-danger {
background: #fff1f1;
color: #b42318;
border: 1px solid #ffd5d5;
}
.auth-alert-success {
background: #effbf3;
color: #16733a;
border: 1px solid #ccefd9;
}
.auth-alert i {
margin-top: 4px;
}
.auth-alert ul {
margin: 7px 0 0;
padding-right: 18px;
}
/* =========================================
LOGIN OPTIONS
========================================= */
.login-options {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.remember-me {
display: flex;
align-items: center;
gap: 7px;
cursor: pointer;
font-size: 13px;
color: #666;
}
.remember-me input {
cursor: pointer;
}
/* =========================================
BUTTON
========================================= */
.auth-btn {
width: 100%;
height: 52px;
border: none;
border-radius: 10px;
background: #1473e6;
color: #fff;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: all 0.25s ease;
}
.auth-btn:hover {
background: #0d5fc5;
transform: translateY(-1px);
box-shadow:
0 8px 20px rgba(20, 115, 230, 0.22);
}
.auth-btn i {
margin-left: 7px;
}
/* =========================================
FOOTER
========================================= */
.auth-footer {
margin-top: 25px;
padding-top: 22px;
border-top: 1px solid #eee;
text-align: center;
color: #777;
font-size: 14px;
}
.auth-footer a {
color: #1473e6;
font-weight: 700;
text-decoration: none;
margin-right: 5px;
}
.auth-footer a:hover {
text-decoration: underline;
}
/* =========================================
RESPONSIVE
========================================= */
@media (max-width: 767px) {
.auth-page {
padding: 30px 15px;
}
.auth-card {
padding: 25px 18px;
border-radius: 14px;
}
.auth-header h1 {
font-size: 23px;
}
.form-section-title {
font-size: 16px;
}
}
+190
View File
@@ -0,0 +1,190 @@
@extends('layouts.app')
@section('title', 'ورود | آژانس مسافرتی')
@section('content')
<div class="auth-page">
<div class="container">
<div class="auth-card login-card">
{{-- عنوان --}}
<div class="auth-header">
<div class="auth-icon">
<i class="fa fa-sign-in"></i>
</div>
<h1>
ورود به حساب کاربری
</h1>
<p>
برای ورود، نام کاربری یا ایمیل و رمز عبور خود را وارد کنید.
</p>
</div>
{{-- خطاها --}}
@if($errors->any())
<div class="auth-alert auth-alert-danger">
<i class="fa fa-exclamation-circle"></i>
<div>
<strong>
ورود انجام نشد.
</strong>
<ul>
@foreach($errors->all() as $error)
<li>
{{ $error }}
</li>
@endforeach
</ul>
</div>
</div>
@endif
{{-- پیام موفقیت --}}
@if(session('success'))
<div class="auth-alert auth-alert-success">
<i class="fa fa-check-circle"></i>
{{ session('success') }}
</div>
@endif
{{-- فرم --}}
<form
action="{{ route('auth.login.post') }}"
method="POST"
class="auth-form"
>
@csrf
<div class="form-group">
<label for="login">
ایمیل یا نام کاربری
</label>
<input
type="text"
id="login"
name="login"
value="{{ old('login') }}"
class="form-control"
placeholder="ایمیل یا نام کاربری"
dir="ltr"
autocomplete="username"
required
>
</div>
<div class="form-group">
<label for="password">
رمز عبور
</label>
<input
type="password"
id="password"
name="password"
class="form-control"
placeholder="رمز عبور"
autocomplete="current-password"
required
>
</div>
{{-- مرا به خاطر بسپار --}}
<div class="login-options">
<label class="remember-me">
<input
type="checkbox"
name="remember"
value="1"
{{ old('remember') ? 'checked' : '' }}
>
<span>
مرا به خاطر بسپار
</span>
</label>
</div>
{{-- دکمه --}}
<button
type="submit"
class="auth-btn"
>
<i class="fa fa-sign-in"></i>
ورود به حساب
</button>
</form>
{{-- ثبت نام --}}
<div class="auth-footer">
حساب کاربری ندارید؟
<a href="{{ route('auth.register.index') }}">
ثبت‌نام کنید
</a>
</div>
</div>
</div>
</div>
@endsection
+659
View File
@@ -0,0 +1,659 @@
@extends('layouts.app')
@section('title', 'ثبت نام | آژانس مسافرتی')
@section('content')
<div class="auth-page">
<div class="container">
<div class="auth-card register-card">
{{-- =========================================
عنوان
========================================== --}}
<div class="auth-header">
<div class="auth-icon">
<i class="fa fa-user-plus"></i>
</div>
<h1>
ایجاد حساب کاربری
</h1>
<p>
برای استفاده از امکانات سایت، اطلاعات خود را وارد کنید.
</p>
</div>
{{-- =========================================
خطاهای کلی
========================================== --}}
@if($errors->any())
<div class="auth-alert auth-alert-danger">
<i class="fa fa-exclamation-circle"></i>
<div>
<strong>
لطفاً خطاهای زیر را بررسی کنید:
</strong>
<ul>
@foreach($errors->all() as $error)
<li>
{{ $error }}
</li>
@endforeach
</ul>
</div>
</div>
@endif
{{-- =========================================
فرم ثبت نام
========================================== --}}
<form
action="{{ route('auth.register.post') }}"
method="POST"
enctype="multipart/form-data"
class="auth-form"
>
@csrf
{{-- =====================================
اطلاعات شخصی
====================================== --}}
<div class="form-section">
<div class="form-section-title">
<i class="fa fa-user"></i>
اطلاعات شخصی
</div>
<div class="row">
{{-- نام --}}
<div class="col-md-6">
<div class="form-group">
<label for="first_name">
نام
<span>*</span>
</label>
<input
type="text"
id="first_name"
name="first_name"
value="{{ old('first_name') }}"
class="form-control"
placeholder="نام خود را وارد کنید"
maxlength="100"
required
>
</div>
</div>
{{-- نام خانوادگی --}}
<div class="col-md-6">
<div class="form-group">
<label for="last_name">
نام خانوادگی
<span>*</span>
</label>
<input
type="text"
id="last_name"
name="last_name"
value="{{ old('last_name') }}"
class="form-control"
placeholder="نام خانوادگی خود را وارد کنید"
maxlength="100"
required
>
</div>
</div>
{{-- کد ملی --}}
<div class="col-md-6">
<div class="form-group">
<label for="national_code">
کد ملی
<span>*</span>
</label>
<input
type="text"
id="national_code"
name="national_code"
value="{{ old('national_code') }}"
class="form-control"
placeholder="مثلاً 0012345678"
maxlength="10"
inputmode="numeric"
required
>
</div>
</div>
{{-- جنسیت --}}
<div class="col-md-6">
<div class="form-group">
<label for="gender">
جنسیت
<span>*</span>
</label>
<select
id="gender"
name="gender"
class="form-control"
required
>
<option value="">
انتخاب کنید
</option>
<option
value="1"
{{ old('gender') == 1 ? 'selected' : '' }}
>
مرد
</option>
<option
value="2"
{{ old('gender') == 2 ? 'selected' : '' }}
>
زن
</option>
</select>
</div>
</div>
{{-- وضعیت تأهل --}}
<div class="col-md-6">
<div class="form-group">
<label for="marital_status">
وضعیت تأهل
<span>*</span>
</label>
<select
id="marital_status"
name="marital_status"
class="form-control"
required
>
<option value="">
انتخاب کنید
</option>
<option
value="1"
{{ old('marital_status') == 1 ? 'selected' : '' }}
>
مجرد
</option>
<option
value="2"
{{ old('marital_status') == 2 ? 'selected' : '' }}
>
متأهل
</option>
<option
value="3"
{{ old('marital_status') == 3 ? 'selected' : '' }}
>
متارکه
</option>
</select>
</div>
</div>
{{-- نام همسر --}}
<div class="col-md-6">
<div class="form-group">
<label for="spouse_name">
نام و نام خانوادگی همسر
</label>
<input
type="text"
id="spouse_name"
name="spouse_name"
value="{{ old('spouse_name') }}"
class="form-control"
placeholder="در صورت تأهل وارد کنید"
maxlength="200"
>
</div>
</div>
</div>
</div>
{{-- =====================================
اطلاعات تماس
====================================== --}}
<div class="form-section">
<div class="form-section-title">
<i class="fa fa-phone"></i>
اطلاعات تماس
</div>
<div class="row">
{{-- موبایل --}}
<div class="col-md-6">
<div class="form-group">
<label for="mobile">
شماره همراه
<span>*</span>
</label>
<input
type="text"
id="mobile"
name="mobile"
value="{{ old('mobile') }}"
class="form-control"
placeholder="09123456789"
maxlength="11"
inputmode="numeric"
required
>
</div>
</div>
{{-- ایمیل --}}
<div class="col-md-6">
<div class="form-group">
<label for="email">
ایمیل
<span>*</span>
</label>
<input
type="email"
id="email"
name="email"
value="{{ old('email') }}"
class="form-control"
placeholder="example@email.com"
maxlength="150"
dir="ltr"
required
>
</div>
</div>
</div>
</div>
{{-- =====================================
اطلاعات حساب
====================================== --}}
<div class="form-section">
<div class="form-section-title">
<i class="fa fa-lock"></i>
اطلاعات حساب کاربری
</div>
<div class="row">
{{-- نام کاربری --}}
<div class="col-md-6">
<div class="form-group">
<label for="username">
نام کاربری
<span>*</span>
</label>
<input
type="text"
id="username"
name="username"
value="{{ old('username') }}"
class="form-control"
placeholder="مثلاً ali123"
maxlength="100"
dir="ltr"
pattern="[A-Za-z0-9]+"
required
>
<small class="form-help">
فقط حروف انگلیسی و اعداد مجاز است.
</small>
</div>
</div>
{{-- رمز --}}
<div class="col-md-6">
<div class="form-group">
<label for="password">
رمز عبور
<span>*</span>
</label>
<input
type="password"
id="password"
name="password"
class="form-control"
placeholder="رمز عبور"
minlength="8"
required
>
</div>
</div>
{{-- تکرار رمز --}}
<div class="col-md-6">
<div class="form-group">
<label for="password_confirmation">
تکرار رمز عبور
</label>
<input
type="password"
id="password_confirmation"
name="password_confirmation"
class="form-control"
placeholder="تکرار رمز عبور"
minlength="8"
>
</div>
</div>
</div>
</div>
{{-- =====================================
استان و شهرستان
====================================== --}}
<div class="form-section">
<div class="form-section-title">
<i class="fa fa-map-marker"></i>
محل سکونت
</div>
<div class="row">
{{-- استان --}}
<div class="col-md-6">
<div class="form-group">
<label for="province_id">
استان
</label>
<select
name="province_id"
id="province_id"
class="form-control"
>
<option value="">
انتخاب استان
</option>
@if(isset($provinces))
@foreach($provinces as $province)
<option
value="{{ $province->id }}"
{{ old('province_id') == $province->id ? 'selected' : '' }}
>
{{ $province->name }}
</option>
@endforeach
@endif
</select>
</div>
</div>
{{-- شهرستان --}}
<div class="col-md-6">
<div class="form-group">
<label for="city_id">
شهرستان
</label>
<select
name="city_id"
id="city_id"
class="form-control"
>
<option value="">
ابتدا استان را انتخاب کنید
</option>
</select>
</div>
</div>
</div>
</div>
{{-- =====================================
عکس
====================================== --}}
<div class="form-section">
<div class="form-section-title">
<i class="fa fa-image"></i>
تصویر پروفایل
</div>
<div class="form-group">
<label for="image">
عکس
</label>
<input
type="file"
id="image"
name="image"
class="form-control"
accept="image/jpeg,image/png,image/jpg"
>
<small class="form-help">
فرمت‌های مجاز:
JPG ، JPEG ، PNG
<br>
حداکثر حجم:
800 کیلوبایت
</small>
</div>
</div>
{{-- =====================================
دکمه ثبت
====================================== --}}
<div class="auth-submit">
<button
type="submit"
class="auth-btn"
>
<i class="fa fa-user-plus"></i>
ایجاد حساب کاربری
</button>
</div>
</form>
{{-- =====================================
ورود
====================================== --}}
<div class="auth-footer">
قبلاً حساب ساخته‌اید؟
<a href="{{ route('auth.login.index') }}">
وارد شوید
</a>
</div>
</div>
</div>
</div>
@endsection