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;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'phone',
|
||||
'role',
|
||||
'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.
|
||||
*
|
||||
* @var list<string>
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'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
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
protected $casts = [
|
||||
'birth_date' => 'date',
|
||||
'is_approved' => 'boolean',
|
||||
'approved_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the user's full name
|
||||
*/
|
||||
public function getFullNameAttribute()
|
||||
{
|
||||
return "{$this->first_name} {$this->last_name}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
web: __DIR__ . '/../routes/web.php',
|
||||
api: __DIR__ . '/../routes/api.php',
|
||||
commands: __DIR__ . '/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
$middleware->validateCsrfTokens(except: [
|
||||
'api/*',
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/sanctum": "^4.2",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
"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",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "d3c16cb86c42230c6c023d9a5d9bcf42",
|
||||
"content-hash": "8f387a0734f3bf879214e4aa2fca6e2f",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
|
||||
@@ -40,6 +40,11 @@ return [
|
||||
'driver' => 'session',
|
||||
'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
|
||||
|
||||
use App\Http\Controllers\AuthController;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/user', function (Request $request) {
|
||||
Route::post('/auth/register', [AuthController::class, 'register']);
|
||||
Route::post('/auth/login', [AuthController::class, 'login']);
|
||||
|
||||
// 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();
|
||||
})->middleware('auth:sanctum');
|
||||
});
|
||||
});
|
||||
|
||||
Route::get('/test', function () {
|
||||
return response()->json(['message' => 'API is working!']);
|
||||
});
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم