feat: Authentication

- Update User model
- Create authentication controller
- Update api router
هذا الالتزام موجود في:
Osama
2025-11-30 20:06:51 +03:00
الأصل 35b86f1779
التزام be6a3b607a
10 ملفات معدلة مع 332 إضافات و24 حذوفات

عرض الملف

@@ -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'
]);
}
}

عرض الملف

@@ -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',
return [ 'is_approved' => 'boolean',
'email_verified_at' => 'datetime', 'approved_at' => 'datetime',
'password' => 'hashed',
]; ];
/**
* 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__)) 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 مولّد
عرض الملف

@@ -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',
],
], ],
/* /*

عرض الملف

@@ -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']);
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(); return $request->user();
})->middleware('auth:sanctum'); });
});
Route::get('/test', function () {
return response()->json(['message' => 'API is working!']);
});