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:
hamed
2026-06-10 11:12:15 +03:30
parent c7ae591e49
commit e9075e8c92
18 changed files with 3533 additions and 142 deletions
+378
View File
@@ -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
{