feat(auth): implement authentication endpoints with registration and login functionality

This commit is contained in:
2026-06-05 19:19:03 +03:30
parent 802326336a
commit 0ddd54dc66
15 changed files with 961 additions and 3 deletions

View File

@@ -0,0 +1,156 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Http\Requests\Auth\RegisterRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
class AuthController extends Controller
{
#[OA\Post(
path: '/auth/register',
description: 'Register a new user account',
summary: 'Register',
tags: ['Auth'],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['name', 'mobile', 'email', 'password'],
properties: [
new OA\Property(property: 'name', type: 'string', example: 'علی رضایی'),
new OA\Property(property: 'mobile', type: 'string', example: '09123456789'),
new OA\Property(property: 'email', type: 'string', format: 'email', example: 'ali@example.com'),
new OA\Property(property: 'password', type: 'string', format: 'password', example: 'password123'),
]
)
),
responses: [
new OA\Response(
response: 201,
description: 'Registered successfully',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'message', type: 'string', example: 'ثبت‌نام با موفقیت انجام شد.'),
new OA\Property(
property: 'data',
properties: [
new OA\Property(property: 'user', ref: '#/components/schemas/User'),
new OA\Property(property: 'token', type: 'string'),
],
type: 'object'
),
]
)
),
new OA\Response(response: 422, description: 'Validation error'),
]
)]
public function register(RegisterRequest $request): JsonResponse
{
$user = User::query()->create([
'name' => $request->validated('name'),
'username' => $this->generateUsername($request->validated('email')),
'mobile' => $request->validated('mobile'),
'email' => $request->validated('email'),
'password' => $request->validated('password'),
]);
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json([
'message' => 'ثبت‌نام با موفقیت انجام شد.',
'data' => [
'user' => new UserResource($user),
'token' => $token,
],
], 201);
}
#[OA\Post(
path: '/auth/login',
description: 'Login with email or username',
summary: 'Login',
tags: ['Auth'],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['login', 'password'],
properties: [
new OA\Property(property: 'login', description: 'Email or username', type: 'string', example: 'ali@example.com'),
new OA\Property(property: 'password', type: 'string', format: 'password', example: 'password123'),
]
)
),
responses: [
new OA\Response(
response: 200,
description: 'Logged in successfully',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'message', type: 'string', example: 'ورود با موفقیت انجام شد.'),
new OA\Property(
property: 'data',
properties: [
new OA\Property(property: 'user', ref: '#/components/schemas/User'),
new OA\Property(property: 'token', type: 'string'),
],
type: 'object'
),
]
)
),
new OA\Response(response: 422, description: 'Invalid credentials'),
]
)]
public function login(LoginRequest $request): JsonResponse
{
$login = $request->validated('login');
$user = User::query()
->where(function ($query) use ($login): void {
$query->where('email', $login)
->orWhere('username', $login);
})
->first();
if (! $user || ! Hash::check($request->validated('password'), $user->password)) {
return response()->json([
'message' => 'اطلاعات ورود نادرست است.',
'errors' => [
'login' => ['ایمیل یا نام کاربری یا رمز عبور اشتباه است.'],
],
], 422);
}
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json([
'message' => 'ورود با موفقیت انجام شد.',
'data' => [
'user' => new UserResource($user),
'token' => $token,
],
]);
}
private function generateUsername(string $email): string
{
$base = Str::slug(Str::before($email, '@'), '') ?: 'user';
$username = $base;
$counter = 1;
while (User::query()->where('username', $username)->exists()) {
$username = $base.$counter;
$counter++;
}
return $username;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'login' => ['required', 'string'],
'password' => ['required', 'string'],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'login.required' => 'ایمیل یا نام کاربری الزامی است.',
'password.required' => 'رمز عبور الزامی است.',
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'mobile' => ['required', 'string', 'regex:/^09\d{9}$/', 'unique:users,mobile'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'],
'password' => ['required', 'string', 'min:8'],
];
}
/**
* @return array<string, string>
*/
public function messages(): array
{
return [
'name.required' => 'نام و نام خانوادگی الزامی است.',
'mobile.required' => 'شماره موبایل الزامی است.',
'mobile.regex' => 'فرمت شماره موبایل صحیح نیست.',
'mobile.unique' => 'این شماره موبایل قبلاً ثبت شده است.',
'email.required' => 'ایمیل الزامی است.',
'email.email' => 'فرمت ایمیل صحیح نیست.',
'email.unique' => 'این ایمیل قبلاً ثبت شده است.',
'password.required' => 'رمز عبور الزامی است.',
'password.min' => 'رمز عبور باید حداقل ۸ کاراکتر باشد.',
];
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Resources;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use OpenApi\Attributes as OA;
#[OA\Schema(
schema: 'User',
properties: [
new OA\Property(property: 'id', type: 'integer', example: 1),
new OA\Property(property: 'name', type: 'string', example: 'علی رضایی'),
new OA\Property(property: 'username', type: 'string', example: 'ali'),
new OA\Property(property: 'email', type: 'string', format: 'email', example: 'ali@example.com'),
new OA\Property(property: 'mobile', type: 'string', example: '09123456789'),
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
]
)]
/** @mixin User */
class UserResource extends JsonResource
{
/**
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'username' => $this->username,
'email' => $this->email,
'mobile' => $this->mobile,
'created_at' => $this->created_at?->toIso8601String(),
];
}
}

View File

@@ -12,13 +12,14 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
#[Fillable(['name', 'email', 'mobile', 'password'])]
#[Fillable(['name', 'username', 'email', 'mobile', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable implements FilamentUser
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
use HasApiTokens, HasFactory, Notifiable;
/**
* @return BelongsToMany<Role, $this>

14
app/OpenApi.php Normal file
View File

@@ -0,0 +1,14 @@
<?php
namespace App;
use OpenApi\Attributes as OA;
#[OA\Info(
version: '1.0.0',
title: 'Hoshpoint API',
description: 'API documentation for Hoshpoint backend',
)]
#[OA\Server(url: '/api', description: 'API server')]
#[OA\Tag(name: 'Auth', description: 'Authentication endpoints')]
class OpenApi {}