diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php new file mode 100644 index 0000000..390465a --- /dev/null +++ b/app/Http/Controllers/AuthController.php @@ -0,0 +1,128 @@ +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' + ]); + } +} diff --git a/app/Http/Requests/RegisterUserRequest.php b/app/Http/Requests/RegisterUserRequest.php new file mode 100644 index 0000000..4fa8bca --- /dev/null +++ b/app/Http/Requests/RegisterUserRequest.php @@ -0,0 +1,48 @@ +|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.', + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 749c7b7..6a4931a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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 + * @var array */ 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 + * @var array */ 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 + * @var array */ - 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 [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; + 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; } } diff --git a/bootstrap/app.php b/bootstrap/app.php index c3928c5..59c8be9 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -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 { // diff --git a/composer.json b/composer.json index 6ede6a4..bdebea5 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/composer.lock b/composer.lock index 16799d2..e035c4e 100644 --- a/composer.lock +++ b/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", diff --git a/config/auth.php b/config/auth.php index 7d1eb0d..91514f0 100644 --- a/config/auth.php +++ b/config/auth.php @@ -40,6 +40,11 @@ return [ 'driver' => 'session', 'provider' => 'users', ], + + 'api' => [ + 'driver' => 'sanctum', + 'provider' => 'users', + ], ], /* diff --git a/database/migrations/2025_11_25_142549_modify_users_table.php b/database/migrations/2025_11_25_142549_modify_users_table.php new file mode 100644 index 0000000..36969eb --- /dev/null +++ b/database/migrations/2025_11_25_142549_modify_users_table.php @@ -0,0 +1,39 @@ +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 + { + // + } +}; diff --git a/database/migrations/2025_11_25_172134_drop_some_users_table_columns.php b/database/migrations/2025_11_25_172134_drop_some_users_table_columns.php new file mode 100644 index 0000000..6b81110 --- /dev/null +++ b/database/migrations/2025_11_25_172134_drop_some_users_table_columns.php @@ -0,0 +1,29 @@ +dropColumn('email'); + $table->dropColumn('email_verified_at'); + $table->dropColumn('name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/routes/api.php b/routes/api.php index ccc387f..e0d38c1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,8 +1,22 @@ user(); -})->middleware('auth:sanctum'); +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(); + }); +}); + +Route::get('/test', function () { + return response()->json(['message' => 'API is working!']); +});