Add comprehensive project documentation for ClinicPro in CLAUDE.md and README.md
- Introduced CLAUDE.md for internal guidance on project structure, commands, and architecture. - Created README.md with detailed project overview, technology stack, directory structure, setup instructions, API endpoints, authentication flow, and external services.
This commit is contained in:
@@ -8,12 +8,14 @@ use App\Auth\Service\OtpService;
|
||||
use App\Auth\Service\TokenService;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Controller\BaseController;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\RateLimiter\RateLimiterFactory;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||
|
||||
#[OA\Tag(name: 'Auth')]
|
||||
class AuthController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
@@ -27,12 +29,110 @@ class AuthController extends BaseController
|
||||
* Route exists so the router resolves it; PasswordAuthenticator intercepts
|
||||
* and returns the JWT response before this controller body ever runs.
|
||||
*/
|
||||
#[OA\Post(
|
||||
path: '/api/v1/user/login',
|
||||
summary: 'Staff login (Admin / Doctor / Clinic / Secretary)',
|
||||
description: 'ورود با شماره موبایل و رمز عبور — فقط برای کاربران دارای نقش ROLE_ADMIN، ROLE_DOCTOR، ROLE_CLINIC یا ROLE_SECRETARY. کاربران عادی باید از OTP استفاده کنند.',
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['mobile_number', 'password'],
|
||||
properties: [
|
||||
new OA\Property(property: 'mobile_number', type: 'string', example: '09120671713', description: 'شماره موبایل ثبتشده'),
|
||||
new OA\Property(property: 'password', type: 'string', format: 'password', example: 'admin1234', description: 'رمز عبور'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'ورود موفق — JWT و refresh token برگردانده میشود',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'access_token', type: 'string', description: 'JWT — عمر ۱ ساعت'),
|
||||
new OA\Property(property: 'refresh_token', type: 'string', description: 'Refresh token — عمر ۳۰ روز'),
|
||||
new OA\Property(property: 'token_type', type: 'string', example: 'Bearer'),
|
||||
new OA\Property(property: 'expires_in', type: 'integer', example: 3600),
|
||||
new OA\Property(property: 'refresh_token_expires_in', type: 'integer', example: 2592000),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
description: 'اطلاعات ورود نادرست',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||
new OA\Property(
|
||||
property: 'errors',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'code', type: 'string', example: 'ERR_AUTH_005'),
|
||||
new OA\Property(property: 'message', type: 'string'),
|
||||
],
|
||||
type: 'object'
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(response: 403, description: 'کاربر نقش staff ندارد (ROLE_ADMIN/ROLE_DOCTOR/ROLE_CLINIC/ROLE_SECRETARY)'),
|
||||
new OA\Response(response: 429, description: 'تعداد تلاشهای ورود از حد مجاز گذشت'),
|
||||
]
|
||||
)]
|
||||
#[Route('/api/v1/user/login', methods: ['POST'])]
|
||||
public function login(): JsonResponse
|
||||
{
|
||||
return $this->error(ErrorCodes::ERR_AUTH_005, ErrorCodes::message(ErrorCodes::ERR_AUTH_005), 401);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/v1/user/send-code',
|
||||
summary: 'Send OTP code to mobile number',
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['mobile'],
|
||||
properties: [
|
||||
new OA\Property(property: 'mobile', type: 'string', example: '09123456789'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'OTP sent successfully',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||
new OA\Property(property: 'message', type: 'string', example: 'کد تایید با موفقیت ارسال شد.'),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
description: 'Invalid mobile format',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||
new OA\Property(
|
||||
property: 'errors',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'code', type: 'string'),
|
||||
new OA\Property(property: 'message', type: 'string'),
|
||||
],
|
||||
type: 'object'
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(response: 429, description: 'Rate limit exceeded'),
|
||||
]
|
||||
)]
|
||||
#[Route('/api/v1/user/send-code', methods: ['POST'])]
|
||||
public function sendCode(Request $request): JsonResponse
|
||||
{
|
||||
@@ -53,6 +153,59 @@ class AuthController extends BaseController
|
||||
return new JsonResponse(['uuid' => $uuid, 'message' => 'کد تایید با موفقیت ارسال شد.']);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/v1/user/verify-code',
|
||||
summary: 'Verify OTP code',
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['uuid', 'code'],
|
||||
properties: [
|
||||
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||
new OA\Property(property: 'code', type: 'string', example: '12345'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Code verified successfully',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||
new OA\Property(
|
||||
property: 'data',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'کد با موفقیت تایید شد.'),
|
||||
],
|
||||
type: 'object'
|
||||
),
|
||||
new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
description: 'Missing or invalid uuid/code',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||
new OA\Property(
|
||||
property: 'errors',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'code', type: 'string'),
|
||||
new OA\Property(property: 'message', type: 'string'),
|
||||
],
|
||||
type: 'object'
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)]
|
||||
#[Route('/api/v1/user/verify-code', methods: ['POST'])]
|
||||
public function verifyCode(Request $request): JsonResponse
|
||||
{
|
||||
@@ -69,6 +222,60 @@ class AuthController extends BaseController
|
||||
return $this->success(['message' => 'کد با موفقیت تایید شد.']);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/api/v1/user/register',
|
||||
summary: 'Register a new user after OTP verification',
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['uuid'],
|
||||
properties: [
|
||||
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||
new OA\Property(property: 'real_name', type: 'string', example: 'علی محمدی'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'User registered successfully',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||
new OA\Property(
|
||||
property: 'data',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'ثبتنام با موفقیت انجام شد.'),
|
||||
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||
],
|
||||
type: 'object'
|
||||
),
|
||||
new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
description: 'Missing uuid',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||
new OA\Property(
|
||||
property: 'errors',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'code', type: 'string'),
|
||||
new OA\Property(property: 'message', type: 'string'),
|
||||
],
|
||||
type: 'object'
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)]
|
||||
#[Route('/api/v1/user/register', methods: ['POST'])]
|
||||
public function register(Request $request): JsonResponse
|
||||
{
|
||||
@@ -94,6 +301,52 @@ class AuthController extends BaseController
|
||||
return $this->success(['message' => 'ثبتنام با موفقیت انجام شد.', 'uuid' => $user->getUuid()], 201);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/oauth/token',
|
||||
summary: 'Issue access and refresh tokens using OTP-verified UUID',
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['grant_type', 'uuid'],
|
||||
properties: [
|
||||
new OA\Property(property: 'grant_type', type: 'string', enum: ['mobile'], example: 'mobile'),
|
||||
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Tokens issued successfully',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'token', type: 'string'),
|
||||
new OA\Property(property: 'refresh_token', type: 'string'),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 400,
|
||||
description: 'Invalid grant_type',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||
new OA\Property(
|
||||
property: 'errors',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'code', type: 'string'),
|
||||
new OA\Property(property: 'message', type: 'string'),
|
||||
],
|
||||
type: 'object'
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)]
|
||||
#[Route('/oauth/token', methods: ['POST'])]
|
||||
public function issueToken(Request $request): JsonResponse
|
||||
{
|
||||
@@ -115,6 +368,51 @@ class AuthController extends BaseController
|
||||
return new JsonResponse($this->tokenService->issueTokens($user));
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/oauth/token/refresh',
|
||||
summary: 'Refresh access token using a refresh token',
|
||||
requestBody: new OA\RequestBody(
|
||||
required: true,
|
||||
content: new OA\JsonContent(
|
||||
required: ['refresh_token'],
|
||||
properties: [
|
||||
new OA\Property(property: 'refresh_token', type: 'string'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Token refreshed successfully',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'token', type: 'string'),
|
||||
new OA\Property(property: 'refresh_token', type: 'string'),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
description: 'Invalid or missing refresh token',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||
new OA\Property(
|
||||
property: 'errors',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'code', type: 'string'),
|
||||
new OA\Property(property: 'message', type: 'string'),
|
||||
],
|
||||
type: 'object'
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)]
|
||||
#[Route('/oauth/token/refresh', methods: ['POST'])]
|
||||
public function refreshToken(Request $request): JsonResponse
|
||||
{
|
||||
@@ -138,6 +436,55 @@ class AuthController extends BaseController
|
||||
return new JsonResponse($tokens);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/oauth/userinfo',
|
||||
summary: 'Get current authenticated user info',
|
||||
security: [['bearerAuth' => []]],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'User info returned successfully',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||
new OA\Property(
|
||||
property: 'data',
|
||||
properties: [
|
||||
new OA\Property(property: 'id', type: 'integer'),
|
||||
new OA\Property(property: 'uuid', type: 'string', format: 'uuid'),
|
||||
new OA\Property(property: 'mobile_number', type: 'string'),
|
||||
new OA\Property(property: 'realName', type: 'string'),
|
||||
new OA\Property(property: 'status', type: 'string'),
|
||||
new OA\Property(property: 'roles', type: 'array', items: new OA\Items(type: 'string')),
|
||||
],
|
||||
type: 'object'
|
||||
),
|
||||
new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
description: 'Unauthenticated',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: false),
|
||||
new OA\Property(
|
||||
property: 'errors',
|
||||
type: 'array',
|
||||
items: new OA\Items(
|
||||
properties: [
|
||||
new OA\Property(property: 'code', type: 'string'),
|
||||
new OA\Property(property: 'message', type: 'string'),
|
||||
],
|
||||
type: 'object'
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)]
|
||||
#[Route('/oauth/userinfo', methods: ['GET'])]
|
||||
public function userInfo(#[CurrentUser] ?User $user): JsonResponse
|
||||
{
|
||||
@@ -155,6 +502,37 @@ class AuthController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
path: '/oauth/logout',
|
||||
summary: 'Logout and optionally revoke refresh token',
|
||||
requestBody: new OA\RequestBody(
|
||||
required: false,
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'refresh_token', type: 'string'),
|
||||
]
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Logged out successfully',
|
||||
content: new OA\JsonContent(
|
||||
properties: [
|
||||
new OA\Property(property: 'success', type: 'boolean', example: true),
|
||||
new OA\Property(
|
||||
property: 'data',
|
||||
properties: [
|
||||
new OA\Property(property: 'message', type: 'string', example: 'خروج با موفقیت انجام شد'),
|
||||
],
|
||||
type: 'object'
|
||||
),
|
||||
new OA\Property(property: 'errors', type: 'array', items: new OA\Items()),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)]
|
||||
#[Route('/oauth/logout', methods: ['POST'])]
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user