feat: Authentication
- Update User model - Create authentication controller - Update api router
هذا الالتزام موجود في:
128
app/Http/Controllers/AuthController.php
Normal file
128
app/Http/Controllers/AuthController.php
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use App\Http\Requests\RegisterUserRequest;
|
||||||
|
|
||||||
|
class AuthController extends Controller
|
||||||
|
{
|
||||||
|
public function register(Request $request)
|
||||||
|
{
|
||||||
|
// If we reach here, validation has already passed!
|
||||||
|
// Laravel automatically validates using our RegisterUserRequest
|
||||||
|
|
||||||
|
try {
|
||||||
|
$shouldValidate = ['phone', 'role', 'first_name', 'last_name', 'password'];
|
||||||
|
foreach ($shouldValidate as $value) {
|
||||||
|
$exist = request()->input($value);
|
||||||
|
if ($exist == null) {
|
||||||
|
return response()->json([
|
||||||
|
"message" => "incomplete data",
|
||||||
|
"field" => $value
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle file uploads
|
||||||
|
$profileImagePath = null;
|
||||||
|
$idImagePath = null;
|
||||||
|
|
||||||
|
if ($request->hasFile('profile_image')) {
|
||||||
|
$profileImagePath = $request->file('profile_image')->store('profiles', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->hasFile('id_image')) {
|
||||||
|
$idImagePath = $request->file('id_image')->store('ids', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
$user = User::create([
|
||||||
|
'phone' => $request->phone,
|
||||||
|
'role' => $request->role,
|
||||||
|
'first_name' => $request->first_name,
|
||||||
|
'last_name' => $request->last_name,
|
||||||
|
'birth_date' => $request->birth_date,
|
||||||
|
'profile_image' => $profileImagePath,
|
||||||
|
'id_image' => $idImagePath,
|
||||||
|
'password' => Hash::make($request->password),
|
||||||
|
// is_approved defaults to false automatically
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Registration successful. Waiting for admin approval.',
|
||||||
|
'user' => [
|
||||||
|
'id' => $user->id,
|
||||||
|
'phone' => $user->phone,
|
||||||
|
'full_name' => $user->full_name,
|
||||||
|
'role' => $user->role,
|
||||||
|
]
|
||||||
|
], 201);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Registration failed',
|
||||||
|
'error' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login(Request $request)
|
||||||
|
{
|
||||||
|
// Basic validation for login
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'phone' => 'required',
|
||||||
|
'password' => 'required'
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find user by phone
|
||||||
|
$user = User::where('phone', $request->phone)->first();
|
||||||
|
|
||||||
|
// Check if user exists and password is correct
|
||||||
|
if (!$user || !Hash::check($request->password, $user->password)) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Invalid credentials'
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is approved
|
||||||
|
if (!$user->is_approved) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Account pending admin approval. Please wait for approval.'
|
||||||
|
], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create API token
|
||||||
|
$token = $user->createToken('auth-token')->plainTextToken;
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Login successful',
|
||||||
|
'token' => $token,
|
||||||
|
'user' => [
|
||||||
|
'id' => $user->id,
|
||||||
|
'phone' => $user->phone,
|
||||||
|
'full_name' => $user->full_name,
|
||||||
|
'role' => $user->role,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} catch (\Exception $exc) {
|
||||||
|
return response()->json([
|
||||||
|
"message" => "failed!",
|
||||||
|
"errors" => $exc->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout(Request $request)
|
||||||
|
{
|
||||||
|
// Delete the current access token
|
||||||
|
$request->user()->currentAccessToken()->delete();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Logged out successfully'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/Http/Requests/RegisterUserRequest.php
Normal file
48
app/Http/Requests/RegisterUserRequest.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class RegisterUserRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true; // Allow all users to register
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'phone' => 'required|unique:users|regex:/^[0-9]{10,15}$/',
|
||||||
|
'role' => 'required|in:tenant,owner',
|
||||||
|
'first_name' => 'required|string|max:255',
|
||||||
|
'last_name' => 'required|string|max:255',
|
||||||
|
'birth_date' => 'required|date|before:-18 years',
|
||||||
|
'profile_image' => 'nullable|image|max:2048', // 2MB max
|
||||||
|
'id_image' => 'required|image|max:2048',
|
||||||
|
'password' => 'required|min:8|confirmed',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom error messages for validator errors.
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'birth_date.before' => 'You must be at least 18 years old to register.',
|
||||||
|
'phone.regex' => 'Phone number must be between 10-15 digits.',
|
||||||
|
'id_image.required' => 'ID image is required for verification.',
|
||||||
|
'phone.unique' => 'This phone number is already registered.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,31 +2,36 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
use HasApiTokens, HasFactory, Notifiable;
|
||||||
use HasFactory, Notifiable;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
*
|
*
|
||||||
* @var list<string>
|
* @var array<int, string>
|
||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'phone',
|
||||||
'email',
|
'role',
|
||||||
'password',
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'profile_image',
|
||||||
|
'birth_date',
|
||||||
|
'id_image',
|
||||||
|
'is_approved',
|
||||||
|
'password', // We'll still use password for authentication
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that should be hidden for serialization.
|
* The attributes that should be hidden for serialization.
|
||||||
*
|
*
|
||||||
* @var list<string>
|
* @var array<int, string>
|
||||||
*/
|
*/
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
'password',
|
'password',
|
||||||
@@ -34,15 +39,53 @@ class User extends Authenticatable
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the attributes that should be cast.
|
* The attributes that should be cast.
|
||||||
*
|
*
|
||||||
* @return array<string, string>
|
* @var array<string, string>
|
||||||
*/
|
*/
|
||||||
protected function casts(): array
|
protected $casts = [
|
||||||
|
'birth_date' => 'date',
|
||||||
|
'is_approved' => 'boolean',
|
||||||
|
'approved_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user's full name
|
||||||
|
*/
|
||||||
|
public function getFullNameAttribute()
|
||||||
{
|
{
|
||||||
return [
|
return "{$this->first_name} {$this->last_name}";
|
||||||
'email_verified_at' => 'datetime',
|
}
|
||||||
'password' => 'hashed',
|
|
||||||
];
|
/**
|
||||||
|
* Check if user is a tenant
|
||||||
|
*/
|
||||||
|
public function isTenant()
|
||||||
|
{
|
||||||
|
return $this->role === 'tenant';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is an owner
|
||||||
|
*/
|
||||||
|
public function isOwner()
|
||||||
|
{
|
||||||
|
return $this->role === 'owner';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is an admin
|
||||||
|
*/
|
||||||
|
public function isAdmin()
|
||||||
|
{
|
||||||
|
return $this->role === 'admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user is approved
|
||||||
|
*/
|
||||||
|
public function isApproved()
|
||||||
|
{
|
||||||
|
return $this->is_approved;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ use Illuminate\Foundation\Configuration\Middleware;
|
|||||||
|
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: __DIR__ . '/../routes/web.php',
|
||||||
api: __DIR__.'/../routes/api.php',
|
api: __DIR__ . '/../routes/api.php',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__ . '/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
//
|
$middleware->validateCsrfTokens(except: [
|
||||||
|
'api/*',
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.2",
|
||||||
"laravel/tinker": "^2.10.1"
|
"laravel/tinker": "^2.10.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|||||||
2
composer.lock
مولّد
2
composer.lock
مولّد
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "d3c16cb86c42230c6c023d9a5d9bcf42",
|
"content-hash": "8f387a0734f3bf879214e4aa2fca6e2f",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ return [
|
|||||||
'driver' => 'session',
|
'driver' => 'session',
|
||||||
'provider' => 'users',
|
'provider' => 'users',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'api' => [
|
||||||
|
'driver' => 'sanctum',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
39
database/migrations/2025_11_25_142549_modify_users_table.php
Normal file
39
database/migrations/2025_11_25_142549_modify_users_table.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
// Remove email and add phone
|
||||||
|
// $table->dropColumn('email');
|
||||||
|
// $table->dropColumn('email_verified_at');
|
||||||
|
|
||||||
|
// Add our custom fields
|
||||||
|
$table->string('phone')->unique();
|
||||||
|
$table->enum('role', ['tenant', 'owner', 'admin'])->default('tenant');
|
||||||
|
$table->string('first_name');
|
||||||
|
$table->string('last_name');
|
||||||
|
$table->string('profile_image')->nullable();
|
||||||
|
$table->date('birth_date');
|
||||||
|
$table->string('id_image')->nullable();
|
||||||
|
$table->boolean('is_approved')->default(false);
|
||||||
|
$table->timestamp('approved_at')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
// Remove email and add phone
|
||||||
|
$table->dropColumn('email');
|
||||||
|
$table->dropColumn('email_verified_at');
|
||||||
|
$table->dropColumn('name');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,8 +1,22 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\AuthController;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/user', function (Request $request) {
|
Route::post('/auth/register', [AuthController::class, 'register']);
|
||||||
return $request->user();
|
Route::post('/auth/login', [AuthController::class, 'login']);
|
||||||
})->middleware('auth:sanctum');
|
|
||||||
|
// Protected routes (require authentication)
|
||||||
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
|
Route::post('/auth/logout', [AuthController::class, 'logout']);
|
||||||
|
|
||||||
|
// Example protected route
|
||||||
|
Route::get('/auth/user', function (Request $request) {
|
||||||
|
return $request->user();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::get('/test', function () {
|
||||||
|
return response()->json(['message' => 'API is working!']);
|
||||||
|
});
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم