diff --git a/docs/OAuth_Cleanup_Summary.md b/docs/OAuth_Cleanup_Summary.md
new file mode 100644
index 0000000..28d9932
--- /dev/null
+++ b/docs/OAuth_Cleanup_Summary.md
@@ -0,0 +1,189 @@
+# خلاصه پاکسازی OAuth Backend
+
+## 🧹 تغییرات انجام شده
+
+### 1. حذف فایلهای غیرضروری
+
+#### حذف شده:
+- `hesabixCore/templates/oauth/authorize.html.twig` - صفحه Twig قدیمی
+- `hesabixCore/templates/oauth/error.html.twig` - صفحه خطای Twig
+- `hesabixCore/templates/oauth/` - پوشه کامل templates
+
+#### دلیل حذف:
+- صفحه authorization حالا در frontend پیادهسازی شده
+- نیازی به template های Twig نیست
+
+### 2. تمیزسازی OAuthController
+
+#### حذف شده:
+- **Method تکراری:** `authorizeApiOld()` - نسخه قدیمی
+- **کد غیرضروری:** بخشهای مربوط به render کردن template ها
+- **Method های اضافی:** کدهای تکراری و غیرضروری
+
+#### بهبود شده:
+- **Error Handling:** به جای render کردن template، JSON response برمیگرداند
+- **Code Structure:** کد تمیزتر و قابل خواندنتر
+- **Performance:** حذف کدهای غیرضروری
+
+### 3. ساختار نهایی OAuthController
+
+#### Endpoints موجود:
+
+```php
+// 1. Authorization endpoint (هدایت به frontend)
+#[Route('/authorize', name: 'oauth_authorize', methods: ['GET'])]
+public function authorize(Request $request): Response
+
+// 2. API endpoint برای frontend
+#[Route('/api/oauth/authorize', name: 'api_oauth_authorize', methods: ['POST'])]
+public function authorizeApi(Request $request): JsonResponse
+
+// 3. Token endpoint
+#[Route('/token', name: 'oauth_token', methods: ['POST'])]
+public function token(Request $request): JsonResponse
+
+// 4. User Info endpoint
+#[Route('/userinfo', name: 'oauth_userinfo', methods: ['GET'])]
+public function userinfo(Request $request): JsonResponse
+
+// 5. Revoke endpoint
+#[Route('/revoke', name: 'oauth_revoke', methods: ['POST'])]
+public function revoke(Request $request): JsonResponse
+
+// 6. Discovery endpoint
+#[Route('/.well-known/oauth-authorization-server', name: 'oauth_discovery', methods: ['GET'])]
+public function discovery(): JsonResponse
+```
+
+## 📊 مقایسه قبل و بعد
+
+### قبل از پاکسازی:
+```
+hesabixCore/
+├── templates/
+│ └── oauth/
+│ ├── authorize.html.twig (7.8KB)
+│ └── error.html.twig (3.1KB)
+└── src/Controller/
+ └── OAuthController.php (442 خط)
+```
+
+### بعد از پاکسازی:
+```
+hesabixCore/
+└── src/Controller/
+ └── OAuthController.php (280 خط)
+```
+
+### کاهش حجم:
+- **حذف شده:** 10.9KB از template files
+- **کاهش خطوط کد:** 162 خط (37% کاهش)
+- **حذف پوشه:** `templates/oauth/`
+
+## ✅ مزایای پاکسازی
+
+### 🚀 عملکرد بهتر
+- کاهش حجم کد
+- حذف کدهای تکراری
+- بهبود سرعت بارگذاری
+
+### 🧹 نگهداری آسانتر
+- کد تمیزتر و قابل خواندن
+- حذف وابستگیهای غیرضروری
+- ساختار سادهتر
+
+### 🔒 امنیت بیشتر
+- حذف کدهای قدیمی که ممکن است آسیبپذیر باشند
+- تمرکز روی endpoint های ضروری
+- Error handling بهتر
+
+### 📱 سازگاری کامل با Frontend
+- تمام منطق UI در frontend
+- Backend فقط API endpoints
+- جداسازی مسئولیتها
+
+## 🔧 نکات فنی
+
+### Error Handling جدید:
+```php
+// قبل
+return $this->render('oauth/error.html.twig', [
+ 'error' => $e->getMessage()
+]);
+
+// بعد
+return new JsonResponse([
+ 'error' => 'invalid_request',
+ 'error_description' => $e->getMessage()
+], Response::HTTP_BAD_REQUEST);
+```
+
+### User Info سادهتر:
+```php
+// قبل: بررسی دستی Authorization header
+$authorization = $request->headers->get('Authorization');
+$token = substr($authorization, 7);
+$accessToken = $this->oauthService->validateAccessToken($token);
+
+// بعد: استفاده از Symfony Security
+$user = $this->getUser();
+if (!$user) {
+ return $this->json(['error' => 'invalid_token'], 401);
+}
+```
+
+## 🧪 تست سیستم
+
+### تست Authorization Flow:
+```bash
+# 1. درخواست مجوز
+curl "https://your-domain.com/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https://your-app.com/callback&response_type=code&scope=read_profile&state=test123"
+
+# 2. باید به frontend هدایت شود
+# https://your-domain.com/u/oauth/authorize?client_id=...
+```
+
+### تست API Endpoint:
+```bash
+# تایید مجوز
+curl -X POST "https://your-domain.com/api/oauth/authorize" \
+ -H "Content-Type: application/json" \
+ -H "X-AUTH-TOKEN: YOUR_TOKEN" \
+ -d '{
+ "client_id": "YOUR_CLIENT_ID",
+ "redirect_uri": "https://your-app.com/callback",
+ "scope": "read_profile",
+ "state": "test123",
+ "approved": true
+ }'
+```
+
+## 📋 چکلیست پاکسازی
+
+### ✅ انجام شده:
+- [x] حذف template files
+- [x] حذف method های تکراری
+- [x] تمیزسازی OAuthController
+- [x] بهبود error handling
+- [x] پاک کردن cache
+- [x] تست عملکرد
+
+### 🔄 بررسی نهایی:
+- [ ] تست کامل OAuth flow
+- [ ] بررسی performance
+- [ ] تست error scenarios
+- [ ] بررسی security
+
+## 🎯 نتیجه نهایی
+
+سیستم OAuth حالا:
+- **سادهتر** و قابل نگهداریتر است
+- **سریعتر** و کارآمدتر است
+- **امنتر** و قابل اعتمادتر است
+- **سازگار** با frontend است
+
+---
+
+**تاریخ پاکسازی:** 2025-08-16
+**وضعیت:** تکمیل ✅
+**توسعهدهنده:** Hesabix Team
\ No newline at end of file
diff --git a/docs/OAuth_Complete_Documentation.md b/docs/OAuth_Complete_Documentation.md
new file mode 100644
index 0000000..5984106
--- /dev/null
+++ b/docs/OAuth_Complete_Documentation.md
@@ -0,0 +1,957 @@
+# مستندات کامل سیستم OAuth 2.0 - Hesabix
+
+## 📋 فهرست مطالب
+
+1. [معرفی OAuth 2.0](#معرفی-oauth-20)
+2. [معماری سیستم](#معماری-سیستم)
+3. [بخش مدیریت](#بخش-مدیریت)
+4. [بخش کاربری](#بخش-کاربری)
+5. [API Documentation](#api-documentation)
+6. [نحوه اتصال](#نحوه-اتصال)
+7. [امنیت](#امنیت)
+8. [مثالهای عملی](#مثالهای-عملی)
+9. [عیبیابی](#عیبیابی)
+10. [پشتیبانی](#پشتیبانی)
+
+---
+
+## 🚀 معرفی OAuth 2.0
+
+OAuth 2.0 یک پروتکل استاندارد برای احراز هویت و مجوزدهی است که به برنامههای خارجی اجازه میدهد بدون نیاز به رمز عبور، به حساب کاربران دسترسی داشته باشند.
+
+### مزایای OAuth 2.0:
+- ✅ **امنیت بالا:** عدم اشتراکگذاری رمز عبور
+- ✅ **کنترل دسترسی:** محدود کردن دسترسیها با Scope
+- ✅ **قابلیت لغو:** امکان لغو دسترسی در هر زمان
+- ✅ **استاندارد:** سازگار با پروتکلهای جهانی
+- ✅ **IP Whitelist:** کنترل دسترسی بر اساس IP
+- ✅ **Rate Limiting:** محدودیت تعداد درخواست
+
+---
+
+## 🏗️ معماری سیستم
+
+### اجزای اصلی:
+
+```
+┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
+│ Client App │ │ OAuth Server │ │ Resource Owner │
+│ (Third Party) │◄──►│ (Hesabix) │◄──►│ (User) │
+└─────────────────┘ └─────────────────┘ └─────────────────┘
+```
+
+### جریان OAuth:
+
+1. **Client Registration:** ثبت برنامه در بخش مدیریت
+2. **Authorization Request:** درخواست مجوز از کاربر
+3. **User Consent:** تأیید کاربر
+4. **Authorization Code:** دریافت کد مجوز
+5. **Token Exchange:** تبدیل کد به Access Token
+6. **Resource Access:** دسترسی به منابع
+
+### پایگاه داده:
+
+```sql
+-- جداول OAuth
+oauth_application -- برنامههای ثبت شده
+oauth_scope -- محدودههای دسترسی
+oauth_authorization_code -- کدهای مجوز موقت
+oauth_access_token -- توکنهای دسترسی
+```
+
+---
+
+## ⚙️ بخش مدیریت
+
+### 1. دسترسی به بخش مدیریت
+
+```
+مدیریت سیستم → تنظیمات سیستم → تب "برنامههای OAuth"
+```
+
+### 2. آمار کلی
+
+سیستم چهار کارت آمار نمایش میدهد:
+
+- **کل برنامهها:** تعداد کل برنامههای ثبت شده
+- **فعال:** تعداد برنامههای فعال
+- **غیرفعال:** تعداد برنامههای غیرفعال
+- **جدید (7 روز):** برنامههای ایجاد شده در هفته گذشته
+
+### 3. ایجاد برنامه جدید
+
+#### مراحل ایجاد:
+
+1. **کلیک روی "ایجاد برنامه جدید"**
+2. **پر کردن فرم:**
+ - **نام برنامه:** نام منحصر به فرد برنامه
+ - **توضیحات:** توضیح کاربرد برنامه
+ - **آدرس وبسایت:** URL اصلی برنامه
+ - **آدرس بازگشت:** URL callback برنامه
+ - **محدودیت درخواست:** تعداد درخواست مجاز در ساعت
+
+#### تنظیمات امنیتی:
+
+##### IP Whitelist:
+```
+- در صورت خالی بودن: از هر IP مجاز است
+- افزودن IP: 192.168.1.1 یا 192.168.1.0/24
+- پشتیبانی از CIDR notation
+- Validation خودکار آدرسهای IP
+```
+
+##### Scope Management:
+```
+read_profile - دسترسی به اطلاعات پروفایل
+write_profile - تغییر اطلاعات پروفایل
+read_business - دسترسی به اطلاعات کسب و کار
+write_business - تغییر اطلاعات کسب و کار
+read_financial - دسترسی به اطلاعات مالی
+write_financial - تغییر اطلاعات مالی
+read_contacts - دسترسی به لیست مخاطبین
+write_contacts - تغییر لیست مخاطبین
+read_documents - دسترسی به اسناد
+write_documents - تغییر اسناد
+admin_access - دسترسی مدیریتی
+```
+
+### 4. مدیریت برنامهها
+
+#### کارت برنامه:
+- **وضعیت:** فعال/غیرفعال با رنگبندی
+- **Client ID:** شناسه یکتا برنامه
+- **تاریخ ایجاد:** تاریخ ثبت برنامه
+- **توضیحات:** شرح برنامه
+
+#### عملیات موجود:
+
+##### دکمههای اصلی:
+- **ویرایش:** تغییر اطلاعات برنامه
+- **آمار:** مشاهده آمار استفاده
+- **فعال/غیرفعال:** تغییر وضعیت برنامه
+
+##### منوی سه نقطه:
+- **بازسازی کلید:** تولید Client Secret جدید
+- **لغو توکنها:** لغو تمام توکنهای فعال
+- **حذف:** حذف کامل برنامه
+
+### 5. اطلاعات امنیتی
+
+پس از ایجاد برنامه، اطلاعات زیر نمایش داده میشود:
+
+```
+Client ID: mL0qT1fkIL6MCJfxIPAh7nM2cQ7ykxEy
+Client Secret: goM7latD9akY83z2O2e9IIEYED3Re6sRMd36f5cUSYHm389PPSqYbFHSX8GtQ9H1
+```
+
+⚠️ **هشدار:** این اطلاعات را در جای امنی ذخیره کنید!
+
+---
+
+## 👤 بخش کاربری
+
+### 1. صفحه مجوزدهی
+
+هنگام اتصال برنامه خارجی، کاربر به صفحه زیر هدایت میشود:
+
+```
+┌─────────────────────────────────────┐
+│ مجوزدهی OAuth │
+├─────────────────────────────────────┤
+│ │
+│ [آیکون برنامه] نام برنامه │
+│ توضیحات برنامه... │
+│ │
+│ این برنامه درخواست دسترسی به: │
+│ ✓ خواندن اطلاعات پروفایل │
+│ ✓ خواندن اطلاعات کسب و کار │
+│ │
+│ [لغو] [تأیید] │
+└─────────────────────────────────────┘
+```
+
+### 2. اطلاعات نمایش داده شده:
+
+- **نام و لوگوی برنامه**
+- **توضیحات برنامه**
+- **محدودههای دسترسی درخواستی**
+- **دکمههای تأیید/لغو**
+
+### 3. تصمیم کاربر:
+
+- **تأیید:** ادامه فرآیند OAuth
+- **لغو:** بازگشت به برنامه اصلی
+
+---
+
+## 📡 API Documentation
+
+### Base URL
+```
+https://your-domain.com/oauth
+```
+
+### 1. Authorization Endpoint
+
+#### درخواست مجوز:
+```http
+GET /oauth/authorize
+```
+
+#### پارامترهای مورد نیاز:
+```javascript
+{
+ "response_type": "code",
+ "client_id": "mL0qT1fkIL6MCJfxIPAh7nM2cQ7ykxEy",
+ "redirect_uri": "https://your-app.com/callback",
+ "scope": "read_profile read_business",
+ "state": "random_string_for_csrf"
+}
+```
+
+#### پاسخ موفق:
+```http
+HTTP/1.1 302 Found
+Location: https://your-app.com/callback?code=AUTHORIZATION_CODE&state=random_string
+```
+
+### 2. Token Endpoint
+
+#### درخواست Access Token:
+```http
+POST /oauth/token
+Content-Type: application/x-www-form-urlencoded
+```
+
+#### پارامترهای مورد نیاز:
+```javascript
+{
+ "grant_type": "authorization_code",
+ "client_id": "mL0qT1fkIL6MCJfxIPAh7nM2cQ7ykxEy",
+ "client_secret": "goM7latD9akY83z2O2e9IIEYED3Re6sRMd36f5cUSYHm389PPSqYbFHSX8GtQ9H1",
+ "code": "AUTHORIZATION_CODE",
+ "redirect_uri": "https://your-app.com/callback"
+}
+```
+
+#### پاسخ موفق:
+```json
+{
+ "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "refresh_token": "def50200...",
+ "scope": "read_profile read_business"
+}
+```
+
+### 3. User Info Endpoint
+
+#### درخواست اطلاعات کاربر:
+```http
+GET /oauth/userinfo
+Authorization: Bearer ACCESS_TOKEN
+```
+
+#### پاسخ موفق:
+```json
+{
+ "id": 123,
+ "email": "user@example.com",
+ "name": "نام کاربر",
+ "profile": {
+ "phone": "+989123456789",
+ "address": "تهران، ایران"
+ }
+}
+```
+
+### 4. Refresh Token Endpoint
+
+#### تمدید Access Token:
+```http
+POST /oauth/token
+Content-Type: application/x-www-form-urlencoded
+```
+
+#### پارامترهای مورد نیاز:
+```javascript
+{
+ "grant_type": "refresh_token",
+ "client_id": "mL0qT1fkIL6MCJfxIPAh7nM2cQ7ykxEy",
+ "client_secret": "goM7latD9akY83z2O2e9IIEYED3Re6sRMd36f5cUSYHm389PPSqYbFHSX8GtQ9H1",
+ "refresh_token": "def50200..."
+}
+```
+
+### 5. Revoke Endpoint
+
+#### لغو Token:
+```http
+POST /oauth/revoke
+Authorization: Bearer ACCESS_TOKEN
+```
+
+#### پارامترهای مورد نیاز:
+```javascript
+{
+ "token": "ACCESS_TOKEN_OR_REFRESH_TOKEN"
+}
+```
+
+### 6. Discovery Endpoint
+
+#### دریافت اطلاعات OAuth Server:
+```http
+GET /.well-known/oauth-authorization-server
+```
+
+#### پاسخ:
+```json
+{
+ "issuer": "https://hesabix.ir",
+ "authorization_endpoint": "https://hesabix.ir/oauth/authorize",
+ "token_endpoint": "https://hesabix.ir/oauth/token",
+ "userinfo_endpoint": "https://hesabix.ir/oauth/userinfo",
+ "revocation_endpoint": "https://hesabix.ir/oauth/revoke",
+ "response_types_supported": ["code"],
+ "grant_types_supported": ["authorization_code", "refresh_token"],
+ "token_endpoint_auth_methods_supported": ["client_secret_post"],
+ "scopes_supported": [
+ "read_profile",
+ "write_profile",
+ "read_business",
+ "write_business",
+ "read_financial",
+ "write_financial",
+ "read_contacts",
+ "write_contacts",
+ "read_documents",
+ "write_documents",
+ "admin_access"
+ ]
+}
+```
+
+---
+
+## 🔗 نحوه اتصال
+
+### 1. ثبت برنامه
+
+ابتدا برنامه خود را در بخش مدیریت ثبت کنید:
+
+```javascript
+// اطلاعات مورد نیاز
+const appInfo = {
+ name: "My Application",
+ description: "توضیح برنامه من",
+ website: "https://myapp.com",
+ redirectUri: "https://myapp.com/oauth/callback",
+ allowedScopes: ["read_profile", "read_business"],
+ ipWhitelist: ["192.168.1.0/24"], // اختیاری
+ rateLimit: 1000
+};
+```
+
+### 2. پیادهسازی OAuth Flow
+
+#### مرحله 1: درخواست مجوز
+```javascript
+function initiateOAuth() {
+ const params = new URLSearchParams({
+ response_type: 'code',
+ client_id: 'YOUR_CLIENT_ID',
+ redirect_uri: 'https://myapp.com/oauth/callback',
+ scope: 'read_profile read_business',
+ state: generateRandomString()
+ });
+
+ window.location.href = `https://hesabix.com/oauth/authorize?${params}`;
+}
+```
+
+#### مرحله 2: دریافت Authorization Code
+```javascript
+// در callback URL
+function handleCallback() {
+ const urlParams = new URLSearchParams(window.location.search);
+ const code = urlParams.get('code');
+ const state = urlParams.get('state');
+
+ if (code && state) {
+ exchangeCodeForToken(code);
+ }
+}
+```
+
+#### مرحله 3: تبدیل Code به Token
+```javascript
+async function exchangeCodeForToken(code) {
+ const response = await fetch('https://hesabix.com/oauth/token', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: new URLSearchParams({
+ grant_type: 'authorization_code',
+ client_id: 'YOUR_CLIENT_ID',
+ client_secret: 'YOUR_CLIENT_SECRET',
+ code: code,
+ redirect_uri: 'https://myapp.com/oauth/callback'
+ })
+ });
+
+ const tokenData = await response.json();
+ // ذخیره token
+ localStorage.setItem('access_token', tokenData.access_token);
+ localStorage.setItem('refresh_token', tokenData.refresh_token);
+}
+```
+
+#### مرحله 4: استفاده از Access Token
+```javascript
+async function getUserInfo() {
+ const response = await fetch('https://hesabix.com/oauth/userinfo', {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ }
+ });
+
+ const userData = await response.json();
+ return userData;
+}
+```
+
+### 3. مدیریت Token
+
+#### تمدید Access Token:
+```javascript
+async function refreshAccessToken() {
+ const response = await fetch('https://hesabix.com/oauth/token', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: new URLSearchParams({
+ grant_type: 'refresh_token',
+ client_id: 'YOUR_CLIENT_ID',
+ client_secret: 'YOUR_CLIENT_SECRET',
+ refresh_token: localStorage.getItem('refresh_token')
+ })
+ });
+
+ const tokenData = await response.json();
+ localStorage.setItem('access_token', tokenData.access_token);
+ localStorage.setItem('refresh_token', tokenData.refresh_token);
+}
+```
+
+#### لغو Token:
+```javascript
+async function revokeToken() {
+ await fetch('https://hesabix.com/oauth/revoke', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('access_token')}`
+ },
+ body: JSON.stringify({
+ token: localStorage.getItem('access_token')
+ })
+ });
+
+ localStorage.removeItem('access_token');
+ localStorage.removeItem('refresh_token');
+}
+```
+
+---
+
+## 🔒 امنیت
+
+### 1. محدودیتهای IP
+
+```javascript
+// بررسی IP در backend
+function checkIpWhitelist($clientIp, $whitelist) {
+ if (empty($whitelist)) {
+ return true; // همه IP ها مجاز
+ }
+
+ foreach ($whitelist as $allowedIp) {
+ if (ipInRange($clientIp, $allowedIp)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+```
+
+### 2. محدودیتهای Scope
+
+```javascript
+// بررسی دسترسی در backend
+function checkScope($requestedScope, $allowedScopes) {
+ $requestedScopes = explode(' ', $requestedScope);
+
+ foreach ($requestedScopes as $scope) {
+ if (!in_array($scope, $allowedScopes)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+```
+
+### 3. Rate Limiting
+
+```javascript
+// محدودیت درخواست
+function checkRateLimit($clientId, $limit) {
+ $requests = getRequestCount($clientId, '1 hour');
+ return $requests < $limit;
+}
+```
+
+### 4. Token Security
+
+- **Access Token:** عمر 1 ساعت
+- **Refresh Token:** عمر 30 روز
+- **JWT Signature:** امضای دیجیتال
+- **Token Revocation:** امکان لغو فوری
+
+### 5. نکات امنیتی مهم
+
+1. **HTTPS اجباری:** تمام ارتباطات باید روی HTTPS باشد
+2. **State Parameter:** همیشه از state parameter استفاده کنید
+3. **Client Secret:** Client Secret را در کد سمت کلاینت قرار ندهید
+4. **Token Storage:** توکنها را در جای امنی ذخیره کنید
+5. **Scope Validation:** همیشه scope ها را بررسی کنید
+
+---
+
+## 💻 مثالهای عملی
+
+### مثال 1: اپلیکیشن وب
+
+```html
+
+
+
+ OAuth Example
+
+
+
+
+
+
+
+```
+
+### مثال 2: اپلیکیشن موبایل
+
+```javascript
+// React Native Example
+import { Linking } from 'react-native';
+
+class OAuthManager {
+ constructor() {
+ this.clientId = 'YOUR_CLIENT_ID';
+ this.redirectUri = 'myapp://oauth/callback';
+ }
+
+ async login() {
+ const authUrl = `https://hesabix.com/oauth/authorize?` +
+ `response_type=code&` +
+ `client_id=${this.clientId}&` +
+ `redirect_uri=${encodeURIComponent(this.redirectUri)}&` +
+ `scope=read_profile&` +
+ `state=${Math.random().toString(36)}`;
+
+ await Linking.openURL(authUrl);
+ }
+
+ async handleCallback(url) {
+ const code = url.match(/code=([^&]*)/)?.[1];
+ if (code) {
+ await this.exchangeCodeForToken(code);
+ }
+ }
+
+ async exchangeCodeForToken(code) {
+ const response = await fetch('https://hesabix.com/oauth/token', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: new URLSearchParams({
+ grant_type: 'authorization_code',
+ client_id: this.clientId,
+ client_secret: 'YOUR_CLIENT_SECRET',
+ code: code,
+ redirect_uri: this.redirectUri
+ })
+ });
+
+ const data = await response.json();
+ await AsyncStorage.setItem('access_token', data.access_token);
+ }
+}
+```
+
+### مثال 3: اپلیکیشن سرور
+
+```python
+# Python Flask Example
+from flask import Flask, request, redirect, session
+import requests
+
+app = Flask(__name__)
+app.secret_key = 'your-secret-key'
+
+CLIENT_ID = 'YOUR_CLIENT_ID'
+CLIENT_SECRET = 'YOUR_CLIENT_SECRET'
+REDIRECT_URI = 'https://myapp.com/oauth/callback'
+
+@app.route('/login')
+def login():
+ auth_url = f'https://hesabix.com/oauth/authorize?' + \
+ f'response_type=code&' + \
+ f'client_id={CLIENT_ID}&' + \
+ f'redirect_uri={REDIRECT_URI}&' + \
+ f'scope=read_profile'
+
+ return redirect(auth_url)
+
+@app.route('/oauth/callback')
+def callback():
+ code = request.args.get('code')
+
+ # تبدیل code به token
+ token_response = requests.post('https://hesabix.com/oauth/token', data={
+ 'grant_type': 'authorization_code',
+ 'client_id': CLIENT_ID,
+ 'client_secret': CLIENT_SECRET,
+ 'code': code,
+ 'redirect_uri': REDIRECT_URI
+ })
+
+ token_data = token_response.json()
+ session['access_token'] = token_data['access_token']
+
+ return 'Login successful!'
+
+@app.route('/user-info')
+def user_info():
+ headers = {'Authorization': f"Bearer {session['access_token']}"}
+ response = requests.get('https://hesabix.com/oauth/userinfo', headers=headers)
+
+ return response.json()
+```
+
+### مثال 4: کلاس کامل JavaScript
+
+```javascript
+class HesabixOAuth {
+ constructor(clientId, redirectUri) {
+ this.clientId = clientId;
+ this.redirectUri = redirectUri;
+ this.baseUrl = 'https://hesabix.com';
+ }
+
+ // شروع فرآیند OAuth
+ authorize(scopes = ['read_profile']) {
+ const state = this.generateState();
+ const params = new URLSearchParams({
+ client_id: this.clientId,
+ redirect_uri: this.redirectUri,
+ response_type: 'code',
+ scope: scopes.join(' '),
+ state: state
+ });
+
+ localStorage.setItem('oauth_state', state);
+ window.location.href = `${this.baseUrl}/oauth/authorize?${params}`;
+ }
+
+ // پردازش callback
+ async handleCallback() {
+ const urlParams = new URLSearchParams(window.location.search);
+ const code = urlParams.get('code');
+ const state = urlParams.get('state');
+ const savedState = localStorage.getItem('oauth_state');
+
+ if (state !== savedState) {
+ throw new Error('State mismatch');
+ }
+
+ if (!code) {
+ throw new Error('Authorization code not found');
+ }
+
+ const tokens = await this.exchangeCode(code);
+ localStorage.setItem('access_token', tokens.access_token);
+ localStorage.setItem('refresh_token', tokens.refresh_token);
+
+ return tokens;
+ }
+
+ // مبادله کد با توکن
+ async exchangeCode(code) {
+ const response = await fetch(`${this.baseUrl}/oauth/token`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ body: new URLSearchParams({
+ grant_type: 'authorization_code',
+ client_id: this.clientId,
+ client_secret: 'YOUR_CLIENT_SECRET', // در سرور ذخیره شود
+ code: code,
+ redirect_uri: this.redirectUri
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('Token exchange failed');
+ }
+
+ return await response.json();
+ }
+
+ // دریافت اطلاعات کاربر
+ async getUserInfo() {
+ const token = localStorage.getItem('access_token');
+ const response = await fetch(`${this.baseUrl}/oauth/userinfo`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to get user info');
+ }
+
+ return await response.json();
+ }
+
+ // تمدید توکن
+ async refreshToken() {
+ const refreshToken = localStorage.getItem('refresh_token');
+ const response = await fetch(`${this.baseUrl}/oauth/token`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ body: new URLSearchParams({
+ grant_type: 'refresh_token',
+ client_id: this.clientId,
+ client_secret: 'YOUR_CLIENT_SECRET',
+ refresh_token: refreshToken
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('Token refresh failed');
+ }
+
+ const tokens = await response.json();
+ localStorage.setItem('access_token', tokens.access_token);
+ localStorage.setItem('refresh_token', tokens.refresh_token);
+
+ return tokens;
+ }
+
+ generateState() {
+ return Math.random().toString(36).substring(2, 15);
+ }
+}
+
+// استفاده
+const oauth = new HesabixOAuth('YOUR_CLIENT_ID', 'https://yourapp.com/callback');
+
+// شروع OAuth
+oauth.authorize(['read_profile', 'read_business']);
+
+// در صفحه callback
+oauth.handleCallback().then(tokens => {
+ console.log('OAuth successful:', tokens);
+ // دریافت اطلاعات کاربر
+ return oauth.getUserInfo();
+}).then(userInfo => {
+ console.log('User info:', userInfo);
+});
+```
+
+---
+
+## 🔧 عیبیابی
+
+### خطاهای رایج:
+
+#### 1. invalid_client
+**علت:** Client ID یا Secret اشتباه
+**راه حل:** بررسی صحت Client ID و Secret
+
+#### 2. invalid_grant
+**علت:** Authorization Code نامعتبر یا منقضی شده
+**راه حل:** درخواست مجدد Authorization Code
+
+#### 3. invalid_scope
+**علت:** Scope درخواستی مجاز نیست
+**راه حل:** بررسی Scope های مجاز در پنل مدیریت
+
+#### 4. access_denied
+**علت:** کاربر دسترسی را لغو کرده
+**راه حل:** درخواست مجدد از کاربر
+
+#### 5. server_error
+**علت:** خطای سرور
+**راه حل:** بررسی لاگهای سرور
+
+### کدهای خطا:
+
+```javascript
+const errorCodes = {
+ 'invalid_request': 'درخواست نامعتبر',
+ 'invalid_client': 'Client ID یا Secret اشتباه',
+ 'invalid_grant': 'کد مجوز نامعتبر',
+ 'unauthorized_client': 'برنامه مجاز نیست',
+ 'unsupported_grant_type': 'نوع grant پشتیبانی نمیشود',
+ 'invalid_scope': 'Scope نامعتبر',
+ 'access_denied': 'دسترسی رد شد',
+ 'server_error': 'خطای سرور',
+ 'temporarily_unavailable': 'سرویس موقتاً در دسترس نیست'
+};
+```
+
+### لاگها:
+
+```bash
+# مشاهده لاگهای OAuth
+tail -f /var/log/hesabix/oauth.log
+
+# پاک کردن کش
+php bin/console cache:clear
+
+# بررسی وضعیت سرور
+php bin/console debug:router | grep oauth
+```
+
+---
+
+## 📞 پشتیبانی
+
+### اطلاعات تماس:
+
+- **ایمیل:** support@hesabix.com
+- **تلفن:** 021-12345678
+- **ساعات کاری:** شنبه تا چهارشنبه، 9 صبح تا 6 عصر
+- **تلگرام:** @hesabix_support
+
+### سوالات متداول:
+
+#### Q: چگونه Client Secret را تغییر دهم؟
+A: در پنل مدیریت، روی منوی سه نقطه برنامه کلیک کرده و "بازسازی کلید" را انتخاب کنید.
+
+#### Q: آیا میتوانم چندین Redirect URI داشته باشم؟
+A: خیر، هر برنامه فقط یک Redirect URI میتواند داشته باشد.
+
+#### Q: توکنها چه مدت اعتبار دارند؟
+A: Access Token 1 ساعت و Refresh Token 30 روز اعتبار دارد.
+
+#### Q: چگونه IP Whitelist را تنظیم کنم؟
+A: در زمان ایجاد یا ویرایش برنامه، IP های مجاز را اضافه کنید. اگر خالی باشد، از هر IP مجاز است.
+
+#### Q: آیا میتوانم Scope ها را بعداً تغییر دهم؟
+A: بله، در بخش ویرایش برنامه میتوانید Scope ها را تغییر دهید.
+
+### گزارش باگ:
+
+برای گزارش باگ، لطفاً اطلاعات زیر را ارسال کنید:
+
+1. **نوع خطا:** کد خطا و پیام
+2. **مراحل تولید:** مراحل دقیق تولید خطا
+3. **اطلاعات برنامه:** Client ID و نام برنامه
+4. **لاگها:** لاگهای مربوطه
+5. **مرورگر/سیستم عامل:** اطلاعات محیط اجرا
+
+---
+
+## 📚 منابع بیشتر
+
+- [RFC 6749 - OAuth 2.0](https://tools.ietf.org/html/rfc6749)
+- [OAuth 2.0 Security Best Practices](https://tools.ietf.org/html/draft-ietf-oauth-security-topics)
+- [OpenID Connect](https://openid.net/connect/)
+- [OAuth 2.0 Authorization Code Flow](https://auth0.com/docs/protocols/oauth2/oauth2-authorization-code-flow)
+
+---
+
+## 📋 چکلیست پیادهسازی
+
+### قبل از شروع:
+- [ ] برنامه در پنل مدیریت ثبت شده
+- [ ] Client ID و Secret دریافت شده
+- [ ] Redirect URI تنظیم شده
+- [ ] Scope های مورد نیاز تعیین شده
+- [ ] IP Whitelist تنظیم شده (در صورت نیاز)
+
+### پیادهسازی:
+- [ ] Authorization Request پیادهسازی شده
+- [ ] Callback Handler پیادهسازی شده
+- [ ] Token Exchange پیادهسازی شده
+- [ ] Error Handling پیادهسازی شده
+- [ ] Token Storage پیادهسازی شده
+
+### تست:
+- [ ] Authorization Flow تست شده
+- [ ] Token Exchange تست شده
+- [ ] User Info API تست شده
+- [ ] Error Scenarios تست شده
+- [ ] Security Features تست شده
+
+---
+
+**نسخه مستندات:** 1.0
+**تاریخ آخرین بهروزرسانی:** 2025-08-16
+**وضعیت:** فعال ✅
+**توسعهدهنده:** Hesabix Team
\ No newline at end of file
diff --git a/docs/OAuth_Copy_Issue_Fix.md b/docs/OAuth_Copy_Issue_Fix.md
new file mode 100644
index 0000000..0149669
--- /dev/null
+++ b/docs/OAuth_Copy_Issue_Fix.md
@@ -0,0 +1,199 @@
+# رفع مشکل کپی کردن Client ID در OAuth
+
+## 🐛 مشکل گزارش شده
+
+در صفحه مدیریت OAuth، هنگام کلیک روی دکمه کپی Client ID، خطای زیر نمایش داده میشد:
+
+```
+خطا در کپی کردن متن
+```
+
+## 🔍 علت مشکل
+
+مشکل از عدم پشتیبانی مرورگر از `navigator.clipboard` یا عدم دسترسی به آن بود. این API در شرایط زیر ممکن است در دسترس نباشد:
+
+1. **مرورگرهای قدیمی** که از Clipboard API پشتیبانی نمیکنند
+2. **HTTP سایتها** (Clipboard API فقط در HTTPS کار میکند)
+3. **تنظیمات امنیتی مرورگر** که دسترسی به clipboard را محدود کرده
+4. **عدم مجوز کاربر** برای دسترسی به clipboard
+
+## ✅ راهحل پیادهسازی شده
+
+### 1. روش دوگانه کپی کردن
+
+```javascript
+async copyToClipboard(text) {
+ try {
+ // روش اول: استفاده از Clipboard API
+ if (navigator.clipboard && window.isSecureContext) {
+ await navigator.clipboard.writeText(text)
+ this.showOAuthSnackbar('متن در کلیپبورد کپی شد', 'success')
+ } else {
+ // روش دوم: fallback برای مرورگرهای قدیمی
+ this.fallbackCopyToClipboard(text)
+ }
+ } catch (error) {
+ console.error('خطا در کپی:', error)
+ // اگر روش اول شکست خورد، از روش دوم استفاده میکنیم
+ this.fallbackCopyToClipboard(text)
+ }
+}
+```
+
+### 2. روش Fallback
+
+```javascript
+fallbackCopyToClipboard(text) {
+ try {
+ // ایجاد یک المان موقت
+ const textArea = document.createElement('textarea')
+ textArea.value = text
+
+ // تنظیمات استایل برای مخفی کردن المان
+ textArea.style.position = 'fixed'
+ textArea.style.left = '-999999px'
+ textArea.style.top = '-999999px'
+ textArea.style.opacity = '0'
+
+ document.body.appendChild(textArea)
+ textArea.focus()
+ textArea.select()
+
+ // کپی کردن متن
+ const successful = document.execCommand('copy')
+
+ // حذف المان موقت
+ document.body.removeChild(textArea)
+
+ if (successful) {
+ this.showOAuthSnackbar('متن در کلیپبورد کپی شد', 'success')
+ } else {
+ this.showOAuthSnackbar('خطا در کپی کردن متن', 'error')
+ }
+ } catch (error) {
+ console.error('خطا در fallback copy:', error)
+ this.showOAuthSnackbar('خطا در کپی کردن متن', 'error')
+ }
+}
+```
+
+## 🔧 نحوه کارکرد
+
+### مرحله 1: بررسی دسترسی
+- بررسی وجود `navigator.clipboard`
+- بررسی `window.isSecureContext` (HTTPS بودن)
+
+### مرحله 2: روش اول (Clipboard API)
+- استفاده از `navigator.clipboard.writeText()`
+- نمایش پیام موفقیت
+
+### مرحله 3: روش دوم (Fallback)
+- ایجاد المان `textarea` موقت
+- مخفی کردن المان
+- انتخاب متن
+- استفاده از `document.execCommand('copy')`
+- حذف المان موقت
+
+### مرحله 4: نمایش نتیجه
+- نمایش پیام موفقیت یا خطا
+- استفاده از `v-snackbar` برای نمایش
+
+## 📱 سازگاری مرورگرها
+
+### ✅ پشتیبانی کامل:
+- Chrome 66+
+- Firefox 63+
+- Safari 13.1+
+- Edge 79+
+
+### ⚠️ پشتیبانی محدود:
+- مرورگرهای قدیمی (از fallback استفاده میکنند)
+- HTTP سایتها (از fallback استفاده میکنند)
+
+## 🎯 مزایای راهحل
+
+### ✅ قابلیت اطمینان بالا
+- دو روش مختلف برای کپی کردن
+- پشتیبانی از تمام مرورگرها
+- مدیریت خطا
+
+### ✅ تجربه کاربری بهتر
+- نمایش پیامهای واضح
+- عدم شکست در کپی کردن
+- سازگار با UI موجود
+
+### ✅ نگهداری آسان
+- کد تمیز و قابل خواندن
+- کامنتهای توضیحی
+- مدیریت خطای مناسب
+
+## 🧪 تست راهحل
+
+### تست در مرورگرهای مختلف:
+```bash
+# Chrome (HTTPS)
+✅ Clipboard API کار میکند
+
+# Firefox (HTTPS)
+✅ Clipboard API کار میکند
+
+# Safari (HTTPS)
+✅ Clipboard API کار میکند
+
+# HTTP سایتها
+✅ Fallback method کار میکند
+
+# مرورگرهای قدیمی
+✅ Fallback method کار میکند
+```
+
+### تست عملکرد:
+1. کلیک روی دکمه کپی Client ID
+2. بررسی نمایش پیام موفقیت
+3. تست کپی کردن در برنامههای دیگر
+4. تست در شرایط مختلف (HTTPS/HTTP)
+
+## 🔒 نکات امنیتی
+
+### ✅ امنیت حفظ شده:
+- استفاده از `isSecureContext` برای بررسی HTTPS
+- عدم نمایش اطلاعات حساس در console
+- مدیریت مناسب خطاها
+
+### ⚠️ محدودیتها:
+- Clipboard API فقط در HTTPS کار میکند
+- نیاز به مجوز کاربر در برخی مرورگرها
+
+## 📋 چکلیست تست
+
+### ✅ انجام شده:
+- [x] تست در Chrome (HTTPS)
+- [x] تست در Firefox (HTTPS)
+- [x] تست در Safari (HTTPS)
+- [x] تست در HTTP سایتها
+- [x] تست در مرورگرهای قدیمی
+- [x] بررسی نمایش پیامها
+- [x] تست عملکرد کپی
+
+### 🔄 تستهای اضافی:
+- [ ] تست در محیط production
+- [ ] تست در دستگاههای مختلف
+- [ ] تست در شرایط شبکه ضعیف
+- [ ] تست عملکرد با حجم زیاد داده
+
+## 🎯 نتیجه
+
+مشکل کپی کردن Client ID کاملاً حل شده و حالا:
+
+- ✅ **در تمام مرورگرها کار میکند**
+- ✅ **پیامهای واضح نمایش میدهد**
+- ✅ **مدیریت خطای مناسب دارد**
+- ✅ **سازگار با UI موجود است**
+
+کاربران حالا میتوانند به راحتی Client ID را کپی کنند بدون هیچ مشکلی.
+
+---
+
+**تاریخ رفع:** 2025-08-16
+**وضعیت:** حل شده ✅
+**توسعهدهنده:** Hesabix Team
\ No newline at end of file
diff --git a/docs/OAuth_Frontend_Integration.md b/docs/OAuth_Frontend_Integration.md
new file mode 100644
index 0000000..3cfa4d1
--- /dev/null
+++ b/docs/OAuth_Frontend_Integration.md
@@ -0,0 +1,181 @@
+# یکپارچهسازی OAuth با Frontend
+
+## مشکل اصلی
+
+سیستم OAuth قبلی در backend پیادهسازی شده بود اما frontend از axios استفاده میکند و نیاز به احراز هویت دارد. این باعث میشد که صفحه authorization نتواند با سیستم احراز هویت موجود کار کند.
+
+## راهحل پیادهسازی شده
+
+### 1. صفحه Authorization در Frontend
+
+**فایل:** `webUI/src/views/oauth/authorize.vue`
+
+این صفحه شامل:
+- **احراز هویت خودکار:** بررسی وضعیت لاگین کاربر با axios
+- **نمایش اطلاعات برنامه:** نام، توضیحات، وبسایت
+- **لیست Scope ها:** نمایش محدودههای دسترسی درخواستی
+- **دکمههای تأیید/رد:** با طراحی زیبا و responsive
+- **مدیریت خطا:** نمایش خطاها و loading states
+
+### 2. Route جدید در Router
+
+**فایل:** `webUI/src/router/index.ts`
+
+```javascript
+{
+ path: '/oauth/authorize',
+ name: 'oauth_authorize',
+ component: () => import('../views/oauth/authorize.vue'),
+ meta: {
+ 'title': 'مجوزدهی OAuth',
+ 'login': true
+ }
+}
+```
+
+### 3. API Endpoints جدید در Backend
+
+#### الف) دریافت اطلاعات برنامه بر اساس Client ID
+**Route:** `GET /api/admin/oauth/applications/client/{clientId}`
+
+```php
+#[Route('/applications/client/{clientId}', name: 'api_admin_oauth_applications_by_client_id', methods: ['GET'])]
+public function getApplicationByClientId(string $clientId): JsonResponse
+```
+
+#### ب) API endpoint برای پردازش مجوز
+**Route:** `POST /api/oauth/authorize`
+
+```php
+#[Route('/api/oauth/authorize', name: 'api_oauth_authorize', methods: ['POST'])]
+public function authorizeApi(Request $request): JsonResponse
+```
+
+### 4. تغییرات در OAuthController
+
+**فایل:** `hesabixCore/src/Controller/OAuthController.php`
+
+- **هدایت به Frontend:** به جای نمایش صفحه Twig، کاربر به frontend هدایت میشود
+- **API endpoint جدید:** برای پردازش مجوز از frontend
+
+### 5. تنظیمات Security
+
+**فایل:** `hesabixCore/config/packages/security.yaml`
+
+```yaml
+- { path: ^/api/oauth, roles: ROLE_USER }
+```
+
+## جریان کار جدید
+
+### 1. درخواست مجوز
+```
+GET /oauth/authorize?client_id=...&redirect_uri=...&scope=...&state=...
+```
+
+### 2. هدایت به Frontend
+Backend کاربر را به صفحه frontend هدایت میکند:
+```
+/u/oauth/authorize?client_id=...&redirect_uri=...&scope=...&state=...
+```
+
+### 3. احراز هویت در Frontend
+- بررسی وضعیت لاگین با axios
+- اگر لاگین نیست، هدایت به صفحه لاگین
+- دریافت اطلاعات برنامه از API
+
+### 4. نمایش صفحه مجوزدهی
+- نمایش اطلاعات برنامه
+- لیست scope های درخواستی
+- دکمههای تأیید/رد
+
+### 5. پردازش مجوز
+- ارسال درخواست به `/api/oauth/authorize`
+- ایجاد authorization code
+- هدایت به redirect_uri
+
+## مزایای این روش
+
+### ✅ سازگاری با سیستم موجود
+- استفاده از axios و احراز هویت موجود
+- سازگار با router و navigation guard ها
+
+### ✅ تجربه کاربری بهتر
+- طراحی مدرن و responsive
+- Loading states و error handling
+- UI/UX یکپارچه با بقیه برنامه
+
+### ✅ امنیت بیشتر
+- احراز هویت خودکار
+- بررسی دسترسیها
+- مدیریت خطا
+
+### ✅ قابلیت توسعه
+- کد تمیز و قابل نگهداری
+- جداسازی منطق frontend و backend
+- امکان اضافه کردن ویژگیهای جدید
+
+## تست سیستم
+
+### 1. تست درخواست مجوز
+```bash
+curl "https://your-domain.com/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https://your-app.com/callback&response_type=code&scope=read_profile&state=test123"
+```
+
+### 2. تست API endpoint
+```bash
+curl -X POST "https://your-domain.com/api/oauth/authorize" \
+ -H "Content-Type: application/json" \
+ -H "X-AUTH-TOKEN: YOUR_TOKEN" \
+ -d '{
+ "client_id": "YOUR_CLIENT_ID",
+ "redirect_uri": "https://your-app.com/callback",
+ "scope": "read_profile",
+ "state": "test123",
+ "approved": true
+ }'
+```
+
+### 3. تست دریافت اطلاعات برنامه
+```bash
+curl -X GET "https://your-domain.com/api/admin/oauth/applications/client/YOUR_CLIENT_ID" \
+ -H "X-AUTH-TOKEN: YOUR_TOKEN"
+```
+
+## نکات مهم
+
+### 🔒 امنیت
+- تمام درخواستها باید احراز هویت شوند
+- بررسی دسترسیها در هر مرحله
+- اعتبارسنجی پارامترها
+
+### 🎨 UI/UX
+- طراحی responsive
+- Loading states
+- Error handling
+- Accessibility
+
+### 🔧 نگهداری
+- کد تمیز و قابل خواندن
+- مستندات کامل
+- تستهای مناسب
+
+## آینده
+
+### ویژگیهای پیشنهادی
+- [ ] صفحه callback در frontend
+- [ ] مدیریت توکنها
+- [ ] آمار استفاده
+- [ ] تنظیمات امنیتی پیشرفته
+
+### بهبودها
+- [ ] Caching اطلاعات برنامه
+- [ ] Offline support
+- [ ] Progressive Web App features
+- [ ] Analytics و monitoring
+
+---
+
+**تاریخ ایجاد:** 2025-08-16
+**وضعیت:** فعال ✅
+**توسعهدهنده:** Hesabix Team
\ No newline at end of file
diff --git a/docs/OAuth_README.md b/docs/OAuth_README.md
new file mode 100644
index 0000000..952c3c0
--- /dev/null
+++ b/docs/OAuth_README.md
@@ -0,0 +1,331 @@
+# مستندات OAuth Hesabix
+
+## مقدمه
+
+Hesabix از پروتکل OAuth 2.0 برای احراز هویت برنامههای خارجی استفاده میکند. این مستندات نحوه پیادهسازی OAuth در برنامههای شما را توضیح میدهد.
+
+## مراحل پیادهسازی
+
+### 1. ثبت برنامه
+
+ابتدا باید برنامه خود را در پنل مدیریت Hesabix ثبت کنید:
+
+1. وارد پنل مدیریت شوید
+2. به بخش "تنظیمات سیستم" بروید
+3. تب "برنامههای OAuth" را انتخاب کنید
+4. روی "برنامه جدید" کلیک کنید
+5. اطلاعات برنامه را وارد کنید:
+ - نام برنامه
+ - توضیحات
+ - آدرس وبسایت
+ - آدرس بازگشت (Redirect URI)
+ - محدودههای دسترسی مورد نیاز
+
+### 2. دریافت Client ID و Client Secret
+
+پس از ثبت برنامه، Client ID و Client Secret به شما داده میشود. این اطلاعات را در جای امنی ذخیره کنید.
+
+## محدودههای دسترسی (Scopes)
+
+| Scope | توضیحات |
+|-------|---------|
+| `read_profile` | دسترسی به اطلاعات پروفایل کاربر |
+| `write_profile` | ویرایش اطلاعات پروفایل کاربر |
+| `read_business` | دسترسی به اطلاعات کسبوکار |
+| `write_business` | ویرایش اطلاعات کسبوکار |
+| `read_accounting` | دسترسی به اطلاعات حسابداری |
+| `write_accounting` | ویرایش اطلاعات حسابداری |
+| `read_reports` | دسترسی به گزارشها |
+| `write_reports` | ایجاد و ویرایش گزارشها |
+| `admin` | دسترسی مدیریتی کامل |
+
+## OAuth Flow
+
+### Authorization Code Flow (توصیه شده)
+
+#### مرحله 1: درخواست مجوز
+
+کاربر را به آدرس زیر هدایت کنید:
+
+```
+GET /oauth/authorize
+```
+
+پارامترهای مورد نیاز:
+- `client_id`: شناسه برنامه شما
+- `redirect_uri`: آدرس بازگشت (باید با آدرس ثبت شده مطابقت داشته باشد)
+- `response_type`: همیشه `code`
+- `scope`: محدودههای دسترسی (با فاصله جدا شده)
+- `state`: مقدار تصادفی برای امنیت (اختیاری)
+
+مثال:
+```
+https://hesabix.ir/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.com/callback&response_type=code&scope=read_profile%20read_business&state=random_string
+```
+
+#### مرحله 2: دریافت کد مجوز
+
+پس از تایید کاربر، به آدرس `redirect_uri` با پارامتر `code` هدایت میشود:
+
+```
+https://yourapp.com/callback?code=AUTHORIZATION_CODE&state=random_string
+```
+
+#### مرحله 3: دریافت توکن دسترسی
+
+کد مجوز را با Client Secret مبادله کنید:
+
+```bash
+curl -X POST https://hesabix.ir/oauth/token \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "grant_type=authorization_code" \
+ -d "client_id=YOUR_CLIENT_ID" \
+ -d "client_secret=YOUR_CLIENT_SECRET" \
+ -d "code=AUTHORIZATION_CODE" \
+ -d "redirect_uri=https://yourapp.com/callback"
+```
+
+پاسخ:
+```json
+{
+ "access_token": "ACCESS_TOKEN",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ "refresh_token": "REFRESH_TOKEN",
+ "scope": "read_profile read_business"
+}
+```
+
+### استفاده از توکن دسترسی
+
+برای دسترسی به API ها، توکن را در header قرار دهید:
+
+```bash
+curl -H "Authorization: Bearer ACCESS_TOKEN" \
+ https://hesabix.ir/oauth/userinfo
+```
+
+## API Endpoints
+
+### اطلاعات کاربر
+
+```
+GET /oauth/userinfo
+Authorization: Bearer ACCESS_TOKEN
+```
+
+پاسخ:
+```json
+{
+ "sub": 123,
+ "email": "user@example.com",
+ "name": "نام کاربر",
+ "mobile": "09123456789",
+ "profile": {
+ "full_name": "نام کامل",
+ "mobile": "09123456789",
+ "email": "user@example.com",
+ "date_register": "2024-01-01",
+ "active": true
+ }
+}
+```
+
+### تمدید توکن
+
+```bash
+curl -X POST https://hesabix.ir/oauth/token \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "grant_type=refresh_token" \
+ -d "client_id=YOUR_CLIENT_ID" \
+ -d "client_secret=YOUR_CLIENT_SECRET" \
+ -d "refresh_token=REFRESH_TOKEN"
+```
+
+### لغو توکن
+
+```bash
+curl -X POST https://hesabix.ir/oauth/revoke \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "token=ACCESS_TOKEN" \
+ -d "token_type_hint=access_token"
+```
+
+## Discovery Endpoint
+
+برای دریافت اطلاعات OAuth server:
+
+```
+GET /.well-known/oauth-authorization-server
+```
+
+پاسخ:
+```json
+{
+ "issuer": "https://hesabix.ir",
+ "authorization_endpoint": "https://hesabix.ir/oauth/authorize",
+ "token_endpoint": "https://hesabix.ir/oauth/token",
+ "userinfo_endpoint": "https://hesabix.ir/oauth/userinfo",
+ "revocation_endpoint": "https://hesabix.ir/oauth/revoke",
+ "response_types_supported": ["code"],
+ "grant_types_supported": ["authorization_code", "refresh_token"],
+ "token_endpoint_auth_methods_supported": ["client_secret_post"],
+ "scopes_supported": [
+ "read_profile",
+ "write_profile",
+ "read_business",
+ "write_business",
+ "read_accounting",
+ "write_accounting",
+ "read_reports",
+ "write_reports",
+ "admin"
+ ]
+}
+```
+
+## نکات امنیتی
+
+1. **HTTPS اجباری**: تمام ارتباطات باید روی HTTPS باشد
+2. **State Parameter**: همیشه از state parameter استفاده کنید
+3. **توکنهای کوتاهمدت**: توکنهای دسترسی 1 ساعت اعتبار دارند
+4. **Refresh Token**: برای تمدید توکن از refresh token استفاده کنید
+5. **Client Secret**: Client Secret را در کد سمت کلاینت قرار ندهید
+
+## مثال پیادهسازی (JavaScript)
+
+```javascript
+class HesabixOAuth {
+ constructor(clientId, redirectUri) {
+ this.clientId = clientId;
+ this.redirectUri = redirectUri;
+ this.baseUrl = 'https://hesabix.ir';
+ }
+
+ // شروع فرآیند OAuth
+ authorize(scopes = ['read_profile']) {
+ const state = this.generateState();
+ const params = new URLSearchParams({
+ client_id: this.clientId,
+ redirect_uri: this.redirectUri,
+ response_type: 'code',
+ scope: scopes.join(' '),
+ state: state
+ });
+
+ localStorage.setItem('oauth_state', state);
+ window.location.href = `${this.baseUrl}/oauth/authorize?${params}`;
+ }
+
+ // پردازش callback
+ async handleCallback() {
+ const urlParams = new URLSearchParams(window.location.search);
+ const code = urlParams.get('code');
+ const state = urlParams.get('state');
+ const savedState = localStorage.getItem('oauth_state');
+
+ if (state !== savedState) {
+ throw new Error('State mismatch');
+ }
+
+ if (!code) {
+ throw new Error('Authorization code not found');
+ }
+
+ const tokens = await this.exchangeCode(code);
+ localStorage.setItem('access_token', tokens.access_token);
+ localStorage.setItem('refresh_token', tokens.refresh_token);
+
+ return tokens;
+ }
+
+ // مبادله کد با توکن
+ async exchangeCode(code) {
+ const response = await fetch(`${this.baseUrl}/oauth/token`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ body: new URLSearchParams({
+ grant_type: 'authorization_code',
+ client_id: this.clientId,
+ client_secret: 'YOUR_CLIENT_SECRET', // در سرور ذخیره شود
+ code: code,
+ redirect_uri: this.redirectUri
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('Token exchange failed');
+ }
+
+ return await response.json();
+ }
+
+ // دریافت اطلاعات کاربر
+ async getUserInfo() {
+ const token = localStorage.getItem('access_token');
+ const response = await fetch(`${this.baseUrl}/oauth/userinfo`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to get user info');
+ }
+
+ return await response.json();
+ }
+
+ // تمدید توکن
+ async refreshToken() {
+ const refreshToken = localStorage.getItem('refresh_token');
+ const response = await fetch(`${this.baseUrl}/oauth/token`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ body: new URLSearchParams({
+ grant_type: 'refresh_token',
+ client_id: this.clientId,
+ client_secret: 'YOUR_CLIENT_SECRET',
+ refresh_token: refreshToken
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('Token refresh failed');
+ }
+
+ const tokens = await response.json();
+ localStorage.setItem('access_token', tokens.access_token);
+ localStorage.setItem('refresh_token', tokens.refresh_token);
+
+ return tokens;
+ }
+
+ generateState() {
+ return Math.random().toString(36).substring(2, 15);
+ }
+}
+
+// استفاده
+const oauth = new HesabixOAuth('YOUR_CLIENT_ID', 'https://yourapp.com/callback');
+
+// شروع OAuth
+oauth.authorize(['read_profile', 'read_business']);
+
+// در صفحه callback
+oauth.handleCallback().then(tokens => {
+ console.log('OAuth successful:', tokens);
+ // دریافت اطلاعات کاربر
+ return oauth.getUserInfo();
+}).then(userInfo => {
+ console.log('User info:', userInfo);
+});
+```
+
+## پشتیبانی
+
+برای سوالات و مشکلات مربوط به OAuth، با تیم پشتیبانی Hesabix تماس بگیرید.
\ No newline at end of file
diff --git a/hesabixCore/config/packages/monolog.yaml b/hesabixCore/config/packages/monolog.yaml
index 8c9efa9..adbe839 100644
--- a/hesabixCore/config/packages/monolog.yaml
+++ b/hesabixCore/config/packages/monolog.yaml
@@ -1,6 +1,7 @@
monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
+ - oauth # OAuth specific logs
when@dev:
monolog:
@@ -22,6 +23,12 @@ when@dev:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
+ oauth:
+ type: stream
+ path: "%kernel.logs_dir%/oauth.log"
+ level: info
+ channels: [oauth]
+ formatter: monolog.formatter.json
when@test:
monolog:
@@ -59,3 +66,9 @@ when@prod:
type: stream
channels: [deprecation]
path: php://stderr
+ oauth:
+ type: stream
+ path: "%kernel.logs_dir%/oauth.log"
+ level: info
+ channels: [oauth]
+ formatter: monolog.formatter.json
diff --git a/hesabixCore/config/packages/security.yaml b/hesabixCore/config/packages/security.yaml
index 77e2f5f..73ab524 100644
--- a/hesabixCore/config/packages/security.yaml
+++ b/hesabixCore/config/packages/security.yaml
@@ -29,6 +29,7 @@ security:
custom_authenticators:
- App\Security\ApiKeyAuthenticator
- App\Security\ParttyAuthenticator
+ - App\Security\OAuthAuthenticator
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
@@ -49,6 +50,13 @@ security:
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/api/wordpress/plugin/stats, roles: PUBLIC_ACCESS }
+ - { path: ^/oauth/authorize, roles: PUBLIC_ACCESS }
+ - { path: ^/oauth/token, roles: PUBLIC_ACCESS }
+ - { path: ^/oauth/.well-known/oauth-authorization-server, roles: PUBLIC_ACCESS }
+ - { path: ^/oauth/userinfo, roles: ROLE_USER }
+ - { path: ^/oauth/revoke, roles: ROLE_USER }
+ - { path: ^/api/admin/oauth, roles: ROLE_ADMIN }
+ - { path: ^/api/oauth, roles: ROLE_USER }
- { path: ^/api/acc/*, roles: ROLE_USER }
- { path: ^/hooks/*, roles: ROLE_USER }
- { path: ^/api/app/*, roles: ROLE_USER }
diff --git a/hesabixCore/migrations/Version20250815230230.php b/hesabixCore/migrations/Version20250815230230.php
new file mode 100644
index 0000000..bb8367a
--- /dev/null
+++ b/hesabixCore/migrations/Version20250815230230.php
@@ -0,0 +1,107 @@
+addSql(<<<'SQL'
+ CREATE TABLE oauth_access_token (id INT AUTO_INCREMENT NOT NULL, token VARCHAR(255) NOT NULL, refresh_token VARCHAR(255) DEFAULT NULL, scopes JSON NOT NULL, expires_at DATETIME NOT NULL, created_at DATETIME NOT NULL, last_used_at DATETIME DEFAULT NULL, is_revoked TINYINT(1) NOT NULL, ip_address VARCHAR(255) DEFAULT NULL, user_agent VARCHAR(500) DEFAULT NULL, user_id INT NOT NULL, application_id INT NOT NULL, scope_id INT DEFAULT NULL, UNIQUE INDEX UNIQ_F7FA86A45F37A13B (token), INDEX IDX_F7FA86A4A76ED395 (user_id), INDEX IDX_F7FA86A43E030ACD (application_id), INDEX IDX_F7FA86A4682B5931 (scope_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4
+ SQL);
+ $this->addSql(<<<'SQL'
+ CREATE TABLE oauth_application (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, description VARCHAR(500) DEFAULT NULL, website VARCHAR(255) NOT NULL, redirect_uri VARCHAR(255) NOT NULL, client_id VARCHAR(64) NOT NULL, client_secret VARCHAR(128) NOT NULL, is_active TINYINT(1) NOT NULL, is_verified TINYINT(1) NOT NULL, logo_url VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, allowed_scopes JSON DEFAULT NULL, rate_limit INT DEFAULT 0 NOT NULL, ip_whitelist JSON DEFAULT NULL, owner_id INT NOT NULL, UNIQUE INDEX UNIQ_F87A716A19EB6921 (client_id), INDEX IDX_F87A716A7E3C61F9 (owner_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4
+ SQL);
+ $this->addSql(<<<'SQL'
+ CREATE TABLE oauth_application_oauth_scope (oauth_application_id INT NOT NULL, oauth_scope_id INT NOT NULL, INDEX IDX_E89D70B5A5F55BAB (oauth_application_id), INDEX IDX_E89D70B54857DA2D (oauth_scope_id), PRIMARY KEY(oauth_application_id, oauth_scope_id)) DEFAULT CHARACTER SET utf8mb4
+ SQL);
+ $this->addSql(<<<'SQL'
+ CREATE TABLE oauth_authorization_code (id INT AUTO_INCREMENT NOT NULL, code VARCHAR(255) NOT NULL, redirect_uri VARCHAR(255) NOT NULL, scopes JSON NOT NULL, expires_at DATETIME NOT NULL, is_used TINYINT(1) NOT NULL, created_at DATETIME NOT NULL, state VARCHAR(255) DEFAULT NULL, code_challenge VARCHAR(255) DEFAULT NULL, code_challenge_method VARCHAR(10) DEFAULT NULL, user_id INT NOT NULL, application_id INT NOT NULL, UNIQUE INDEX UNIQ_793B081777153098 (code), INDEX IDX_793B0817A76ED395 (user_id), INDEX IDX_793B08173E030ACD (application_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4
+ SQL);
+ $this->addSql(<<<'SQL'
+ CREATE TABLE oauth_scope (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(100) NOT NULL, description VARCHAR(255) NOT NULL, is_default TINYINT(1) NOT NULL, is_system TINYINT(1) NOT NULL, created_at DATETIME NOT NULL, UNIQUE INDEX UNIQ_87ACBFC25E237E06 (name), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_access_token ADD CONSTRAINT FK_F7FA86A4A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_access_token ADD CONSTRAINT FK_F7FA86A43E030ACD FOREIGN KEY (application_id) REFERENCES oauth_application (id)
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_access_token ADD CONSTRAINT FK_F7FA86A4682B5931 FOREIGN KEY (scope_id) REFERENCES oauth_scope (id)
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_application ADD CONSTRAINT FK_F87A716A7E3C61F9 FOREIGN KEY (owner_id) REFERENCES user (id)
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_application_oauth_scope ADD CONSTRAINT FK_E89D70B5A5F55BAB FOREIGN KEY (oauth_application_id) REFERENCES oauth_application (id) ON DELETE CASCADE
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_application_oauth_scope ADD CONSTRAINT FK_E89D70B54857DA2D FOREIGN KEY (oauth_scope_id) REFERENCES oauth_scope (id) ON DELETE CASCADE
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_authorization_code ADD CONSTRAINT FK_793B0817A76ED395 FOREIGN KEY (user_id) REFERENCES user (id)
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_authorization_code ADD CONSTRAINT FK_793B08173E030ACD FOREIGN KEY (application_id) REFERENCES oauth_application (id)
+ SQL);
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_access_token DROP FOREIGN KEY FK_F7FA86A4A76ED395
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_access_token DROP FOREIGN KEY FK_F7FA86A43E030ACD
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_access_token DROP FOREIGN KEY FK_F7FA86A4682B5931
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_application DROP FOREIGN KEY FK_F87A716A7E3C61F9
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_application_oauth_scope DROP FOREIGN KEY FK_E89D70B5A5F55BAB
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_application_oauth_scope DROP FOREIGN KEY FK_E89D70B54857DA2D
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_authorization_code DROP FOREIGN KEY FK_793B0817A76ED395
+ SQL);
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_authorization_code DROP FOREIGN KEY FK_793B08173E030ACD
+ SQL);
+ $this->addSql(<<<'SQL'
+ DROP TABLE oauth_access_token
+ SQL);
+ $this->addSql(<<<'SQL'
+ DROP TABLE oauth_application
+ SQL);
+ $this->addSql(<<<'SQL'
+ DROP TABLE oauth_application_oauth_scope
+ SQL);
+ $this->addSql(<<<'SQL'
+ DROP TABLE oauth_authorization_code
+ SQL);
+ $this->addSql(<<<'SQL'
+ DROP TABLE oauth_scope
+ SQL);
+ }
+}
diff --git a/hesabixCore/migrations/Version20250816003509.php b/hesabixCore/migrations/Version20250816003509.php
new file mode 100644
index 0000000..2ee8be9
--- /dev/null
+++ b/hesabixCore/migrations/Version20250816003509.php
@@ -0,0 +1,35 @@
+addSql(<<<'SQL'
+ ALTER TABLE oauth_application DROP is_verified
+ SQL);
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql(<<<'SQL'
+ ALTER TABLE oauth_application ADD is_verified TINYINT(1) NOT NULL
+ SQL);
+ }
+}
diff --git a/hesabixCore/src/Command/CleanupOAuthCommand.php b/hesabixCore/src/Command/CleanupOAuthCommand.php
new file mode 100644
index 0000000..36fde9e
--- /dev/null
+++ b/hesabixCore/src/Command/CleanupOAuthCommand.php
@@ -0,0 +1,64 @@
+oauthService = $oauthService;
+ }
+
+ protected function configure(): void
+ {
+ $this
+ ->setHelp('این دستور توکنها و کدهای منقضی شده OAuth را پاکسازی میکند.');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+
+ $io->title('پاکسازی OAuth');
+
+ try {
+ $result = $this->oauthService->cleanupExpiredItems();
+
+ $io->success('پاکسازی OAuth با موفقیت انجام شد.');
+
+ $io->table(
+ ['نوع', 'تعداد حذف شده'],
+ [
+ ['کدهای مجوز منقضی شده', $result['expired_codes']],
+ ['توکنهای منقضی شده', $result['expired_tokens']]
+ ]
+ );
+
+ $total = $result['expired_codes'] + $result['expired_tokens'];
+ if ($total > 0) {
+ $io->info("در مجموع {$total} آیتم منقضی شده پاکسازی شد.");
+ } else {
+ $io->info('هیچ آیتم منقضی شدهای یافت نشد.');
+ }
+
+ return Command::SUCCESS;
+ } catch (\Exception $e) {
+ $io->error('خطا در پاکسازی OAuth: ' . $e->getMessage());
+ return Command::FAILURE;
+ }
+ }
+}
\ No newline at end of file
diff --git a/hesabixCore/src/Command/CreateOAuthScopesCommand.php b/hesabixCore/src/Command/CreateOAuthScopesCommand.php
new file mode 100644
index 0000000..a00bbb1
--- /dev/null
+++ b/hesabixCore/src/Command/CreateOAuthScopesCommand.php
@@ -0,0 +1,64 @@
+oauthService = $oauthService;
+ }
+
+ protected function configure(): void
+ {
+ $this
+ ->setHelp('این دستور محدودههای پیشفرض OAuth را ایجاد میکند.');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+
+ $io->title('ایجاد محدودههای پیشفرض OAuth');
+
+ try {
+ $this->oauthService->createDefaultScopes();
+
+ $io->success('محدودههای پیشفرض OAuth با موفقیت ایجاد شدند.');
+
+ $io->table(
+ ['نام محدوده', 'توضیحات', 'پیشفرض'],
+ [
+ ['read_profile', 'دسترسی به اطلاعات پروفایل کاربر', 'بله'],
+ ['write_profile', 'ویرایش اطلاعات پروفایل کاربر', 'خیر'],
+ ['read_business', 'دسترسی به اطلاعات کسبوکار', 'بله'],
+ ['write_business', 'ویرایش اطلاعات کسبوکار', 'خیر'],
+ ['read_accounting', 'دسترسی به اطلاعات حسابداری', 'خیر'],
+ ['write_accounting', 'ویرایش اطلاعات حسابداری', 'خیر'],
+ ['read_reports', 'دسترسی به گزارشها', 'خیر'],
+ ['write_reports', 'ایجاد و ویرایش گزارشها', 'خیر'],
+ ['admin', 'دسترسی مدیریتی کامل', 'خیر']
+ ]
+ );
+
+ return Command::SUCCESS;
+ } catch (\Exception $e) {
+ $io->error('خطا در ایجاد محدودههای OAuth: ' . $e->getMessage());
+ return Command::FAILURE;
+ }
+ }
+}
\ No newline at end of file
diff --git a/hesabixCore/src/Controller/OAuthApplicationController.php b/hesabixCore/src/Controller/OAuthApplicationController.php
new file mode 100644
index 0000000..abf8e24
--- /dev/null
+++ b/hesabixCore/src/Controller/OAuthApplicationController.php
@@ -0,0 +1,453 @@
+oauthService = $oauthService;
+ $this->extractor = $extractor;
+ $this->logger = $logger;
+ $this->provider = $provider;
+ $this->entityManager = $entityManager;
+ $this->validator = $validator;
+ }
+
+ /**
+ * لیست برنامههای OAuth کاربر
+ */
+ #[Route('/applications', name: 'api_admin_oauth_applications_list', methods: ['GET'])]
+ public function listApplications(): JsonResponse
+ {
+ $user = $this->getUser();
+ if (!$user) {
+ throw $this->createAccessDeniedException();
+ }
+
+ $applications = $this->entityManager->getRepository(\App\Entity\OAuthApplication::class)->findByOwner($user->getId());
+
+ return $this->json($this->extractor->operationSuccess(
+ $this->provider->ArrayEntity2Array($applications, 1, ['owner', 'scopes'])
+ ));
+ }
+
+ /**
+ * ایجاد برنامه OAuth جدید
+ */
+ #[Route('/applications', name: 'api_admin_oauth_applications_create', methods: ['POST'])]
+ public function createApplication(Request $request): JsonResponse
+ {
+ $user = $this->getUser();
+ if (!$user) {
+ throw $this->createAccessDeniedException();
+ }
+
+ $data = json_decode($request->getContent(), true);
+
+ // اعتبارسنجی دادههای ورودی
+ if (!isset($data['name'])) {
+ return $this->json($this->extractor->operationFail('نام الزامی است'));
+ }
+
+ // پشتیبانی از هر دو فرمت redirect_uri و redirectUri
+ $redirectUri = $data['redirect_uri'] ?? $data['redirectUri'] ?? null;
+ if (!$redirectUri) {
+ return $this->json($this->extractor->operationFail('آدرس بازگشت الزامی است'));
+ }
+
+ // بررسی تکراری نبودن نام
+ $existingApp = $this->entityManager->getRepository(OAuthApplication::class)->findOneBy([
+ 'name' => $data['name'],
+ 'owner' => $user
+ ]);
+
+ if ($existingApp) {
+ return $this->json($this->extractor->operationFail('برنامهای با این نام قبلاً وجود دارد'));
+ }
+
+ // ایجاد برنامه جدید
+ $application = new OAuthApplication();
+ $application->setName($data['name']);
+ $application->setDescription($data['description'] ?? '');
+ $application->setWebsite($data['website'] ?? '');
+ $application->setRedirectUri($redirectUri);
+ $application->setOwner($user);
+
+ // تنظیم فیلدهای اختیاری
+ if (isset($data['rateLimit']) || isset($data['rate_limit'])) {
+ $rateLimit = $data['rateLimit'] ?? $data['rate_limit'] ?? 1000;
+ $application->setRateLimit($rateLimit);
+ }
+
+ if (isset($data['allowedScopes']) || isset($data['allowed_scopes'])) {
+ $allowedScopes = $data['allowedScopes'] ?? $data['allowed_scopes'] ?? [];
+ $application->setAllowedScopes($allowedScopes);
+ }
+
+ if (isset($data['ipWhitelist']) || isset($data['ip_whitelist'])) {
+ $ipWhitelist = $data['ipWhitelist'] ?? $data['ip_whitelist'] ?? [];
+ $application->setIpWhitelist($ipWhitelist);
+ }
+
+ // تولید client_id و client_secret
+ $credentials = $this->oauthService->generateClientCredentials();
+ $application->setClientId($credentials['client_id']);
+ $application->setClientSecret($credentials['client_secret']);
+
+ // تنظیم محدودههای پیشفرض (فقط اگر محدودهای تنظیم نشده باشد)
+ if (empty($application->getAllowedScopes())) {
+ $defaultScopes = $this->entityManager->getRepository(OAuthScope::class)->findDefaultScopes();
+ $application->setAllowedScopes(array_map(fn($scope) => $scope->getName(), $defaultScopes));
+ }
+
+ // اعتبارسنجی
+ $errors = $this->validator->validate($application);
+ if (count($errors) > 0) {
+ return $this->json($this->extractor->operationFail('دادههای ورودی نامعتبر است'));
+ }
+
+ $this->entityManager->persist($application);
+ $this->entityManager->flush();
+
+ // ثبت لاگ
+ $this->logger->info('OAuth Application Created', [
+ 'application_name' => $application->getName(),
+ 'user_id' => $user->getId(),
+ 'user_email' => $user->getEmail()
+ ]);
+
+ return $this->json($this->extractor->operationSuccess([
+ 'application' => $this->provider->Entity2Array($application, 1, ['owner', 'scopes']),
+ 'client_id' => $application->getClientId(),
+ 'client_secret' => $application->getClientSecret()
+ ]));
+ }
+
+ /**
+ * ویرایش برنامه OAuth
+ */
+ #[Route('/applications/{id}', name: 'api_admin_oauth_applications_update', methods: ['PUT'])]
+ public function updateApplication(Request $request, int $id): JsonResponse
+ {
+ $user = $this->getUser();
+ if (!$user) {
+ throw $this->createAccessDeniedException();
+ }
+
+ $application = $this->entityManager->getRepository(OAuthApplication::class)->find($id);
+ if (!$application || $application->getOwner()->getId() !== $user->getId()) {
+ throw $this->createNotFoundException('برنامه یافت نشد');
+ }
+
+ $data = json_decode($request->getContent(), true);
+
+ if (isset($data['name'])) {
+ $application->setName($data['name']);
+ }
+ if (isset($data['description'])) {
+ $application->setDescription($data['description']);
+ }
+ if (isset($data['website'])) {
+ $application->setWebsite($data['website']);
+ }
+ if (isset($data['redirect_uri']) || isset($data['redirectUri'])) {
+ $redirectUri = $data['redirect_uri'] ?? $data['redirectUri'];
+ $application->setRedirectUri($redirectUri);
+ }
+ if (isset($data['allowed_scopes']) || isset($data['allowedScopes'])) {
+ $allowedScopes = $data['allowed_scopes'] ?? $data['allowedScopes'];
+ $application->setAllowedScopes($allowedScopes);
+ }
+ if (isset($data['rate_limit']) || isset($data['rateLimit'])) {
+ $rateLimit = $data['rate_limit'] ?? $data['rateLimit'];
+ $application->setRateLimit($rateLimit);
+ }
+ if (isset($data['ip_whitelist']) || isset($data['ipWhitelist'])) {
+ $ipWhitelist = $data['ip_whitelist'] ?? $data['ipWhitelist'] ?? [];
+ $application->setIpWhitelist($ipWhitelist);
+ }
+
+ // اعتبارسنجی
+ $errors = $this->validator->validate($application);
+ if (count($errors) > 0) {
+ return $this->json($this->extractor->operationFail('دادههای ورودی نامعتبر است'));
+ }
+
+ $this->entityManager->flush();
+
+ // ثبت لاگ
+ $this->logger->info('OAuth Application Updated', [
+ 'application_name' => $application->getName(),
+ 'user_id' => $user->getId(),
+ 'user_email' => $user->getEmail()
+ ]);
+
+ return $this->json($this->extractor->operationSuccess(
+ $this->provider->Entity2Array($application, 1, ['owner', 'scopes'])
+ ));
+ }
+
+ /**
+ * حذف برنامه OAuth
+ */
+ #[Route('/applications/{id}', name: 'api_admin_oauth_applications_delete', methods: ['DELETE'])]
+ public function deleteApplication(int $id): JsonResponse
+ {
+ $user = $this->getUser();
+ if (!$user) {
+ throw $this->createAccessDeniedException();
+ }
+
+ $application = $this->entityManager->getRepository(OAuthApplication::class)->find($id);
+ if (!$application || $application->getOwner()->getId() !== $user->getId()) {
+ throw $this->createNotFoundException('برنامه یافت نشد');
+ }
+
+ $appName = $application->getName();
+
+ // لغو تمام توکنهای مربوط به این برنامه
+ $this->entityManager->getRepository(\App\Entity\OAuthAccessToken::class)->revokeApplicationTokens($application->getId());
+
+ $this->entityManager->remove($application);
+ $this->entityManager->flush();
+
+ // ثبت لاگ
+ $this->logger->info('OAuth Application Deleted', [
+ 'application_name' => $appName,
+ 'user_id' => $user->getId(),
+ 'user_email' => $user->getEmail()
+ ]);
+
+ return $this->json($this->extractor->operationSuccess());
+ }
+
+ /**
+ * بازسازی client_secret
+ */
+ #[Route('/applications/{id}/regenerate-secret', name: 'api_admin_oauth_applications_regenerate_secret', methods: ['POST'])]
+ public function regenerateClientSecret(int $id): JsonResponse
+ {
+ $user = $this->getUser();
+ if (!$user) {
+ throw $this->createAccessDeniedException();
+ }
+
+ $application = $this->entityManager->getRepository(OAuthApplication::class)->find($id);
+ if (!$application || $application->getOwner()->getId() !== $user->getId()) {
+ throw $this->createNotFoundException('برنامه یافت نشد');
+ }
+
+ // لغو تمام توکنهای موجود
+ $this->entityManager->getRepository(\App\Entity\OAuthAccessToken::class)->revokeApplicationTokens($application->getId());
+
+ // تولید client_secret جدید
+ $credentials = $this->oauthService->generateClientCredentials();
+ $application->setClientSecret($credentials['client_secret']);
+
+ $this->entityManager->flush();
+
+ // ثبت لاگ
+ $this->logger->info('OAuth Client Secret Regenerated', [
+ 'application_name' => $application->getName(),
+ 'user_id' => $user->getId(),
+ 'user_email' => $user->getEmail()
+ ]);
+
+ return $this->json($this->extractor->operationSuccess([
+ 'client_secret' => $application->getClientSecret()
+ ]));
+ }
+
+ /**
+ * لیست محدودههای دسترسی
+ */
+ #[Route('/scopes', name: 'api_admin_oauth_scopes_list', methods: ['GET'])]
+ public function listScopes(): JsonResponse
+ {
+ $scopes = $this->entityManager->getRepository(OAuthScope::class)->findAll();
+
+ return $this->json($this->extractor->operationSuccess(
+ $this->provider->ArrayEntity2Array($scopes)
+ ));
+ }
+
+ /**
+ * آمار استفاده از برنامه OAuth
+ */
+ #[Route('/applications/{id}/stats', name: 'api_admin_oauth_applications_stats', methods: ['GET'])]
+ public function getApplicationStats(int $id): JsonResponse
+ {
+ $user = $this->getUser();
+ if (!$user) {
+ throw $this->createAccessDeniedException();
+ }
+
+ $application = $this->entityManager->getRepository(OAuthApplication::class)->find($id);
+ if (!$application || $application->getOwner()->getId() !== $user->getId()) {
+ throw $this->createNotFoundException('برنامه یافت نشد');
+ }
+
+ // تعداد توکنهای فعال
+ $activeTokens = $this->entityManager->getRepository(\App\Entity\OAuthAccessToken::class)->findByApplication($application->getId());
+ $activeTokensCount = count(array_filter($activeTokens, fn($token) => $token->isValid()));
+
+ // تعداد توکنهای منقضی شده
+ $expiredTokensCount = count(array_filter($activeTokens, fn($token) => $token->isExpired()));
+
+ // آخرین استفاده
+ $lastUsed = null;
+ if (!empty($activeTokens)) {
+ $lastUsedToken = max($activeTokens, fn($a, $b) => $a->getLastUsedAt() <=> $b->getLastUsedAt());
+ $lastUsed = $lastUsedToken->getLastUsedAt();
+ }
+
+ $stats = [
+ 'total_tokens' => count($activeTokens),
+ 'active_tokens' => $activeTokensCount,
+ 'expired_tokens' => $expiredTokensCount,
+ 'last_used' => $lastUsed,
+ 'created_at' => $application->getCreatedAt(),
+ 'is_active' => $application->isActive()
+ ];
+
+ return $this->json($this->extractor->operationSuccess($stats));
+ }
+
+ /**
+ * لغو تمام توکنهای برنامه
+ */
+ #[Route('/applications/{id}/revoke-tokens', name: 'api_admin_oauth_applications_revoke_tokens', methods: ['POST'])]
+ public function revokeAllTokens(int $id): JsonResponse
+ {
+ $user = $this->getUser();
+ if (!$user) {
+ throw $this->createAccessDeniedException();
+ }
+
+ $application = $this->entityManager->getRepository(OAuthApplication::class)->find($id);
+ if (!$application || $application->getOwner()->getId() !== $user->getId()) {
+ throw $this->createNotFoundException('برنامه یافت نشد');
+ }
+
+ $revokedCount = $this->entityManager->getRepository(\App\Entity\OAuthAccessToken::class)->revokeApplicationTokens($application->getId());
+
+ // ثبت لاگ
+ $this->logger->info('OAuth Tokens Revoked', [
+ 'application_name' => $application->getName(),
+ 'revoked_count' => $revokedCount,
+ 'user_id' => $user->getId(),
+ 'user_email' => $user->getEmail()
+ ]);
+
+ return $this->json($this->extractor->operationSuccess([
+ 'revoked_count' => $revokedCount
+ ]));
+ }
+
+ /**
+ * دریافت اطلاعات برنامه بر اساس Client ID
+ */
+ #[Route('/applications/client/{clientId}', name: 'api_admin_oauth_applications_by_client_id', methods: ['GET'])]
+ public function getApplicationByClientId(string $clientId): JsonResponse
+ {
+ try {
+ $application = $this->entityManager->getRepository(\App\Entity\OAuthApplication::class)->findByClientId($clientId);
+ if (!$application) {
+ return $this->json([
+ 'Success' => false,
+ 'message' => 'برنامه یافت نشد'
+ ], Response::HTTP_NOT_FOUND);
+ }
+
+ return $this->json([
+ 'Success' => true,
+ 'data' => [
+ 'id' => $application->getId(),
+ 'name' => $application->getName(),
+ 'description' => $application->getDescription(),
+ 'website' => $application->getWebsite(),
+ 'redirectUri' => $application->getRedirectUri(),
+ 'clientId' => $application->getClientId(),
+ 'isActive' => $application->isActive(),
+ 'allowedScopes' => $application->getAllowedScopes(),
+ 'createdAt' => $application->getCreatedAt()
+ ]
+ ]);
+
+ } catch (\Exception $e) {
+ $this->logger->error('Error getting application by client ID: ' . $e->getMessage());
+ return $this->json([
+ 'Success' => false,
+ 'message' => 'خطا در دریافت اطلاعات برنامه'
+ ], Response::HTTP_INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ /**
+ * فعال/غیرفعال کردن برنامه OAuth
+ */
+ #[Route('/applications/{id}/toggle-status', name: 'api_admin_oauth_applications_toggle_status', methods: ['POST'])]
+ public function toggleApplicationStatus(int $id): JsonResponse
+ {
+ $user = $this->getUser();
+ if (!$user) {
+ throw $this->createAccessDeniedException();
+ }
+
+ $application = $this->entityManager->getRepository(OAuthApplication::class)->find($id);
+ if (!$application || $application->getOwner()->getId() !== $user->getId()) {
+ throw $this->createNotFoundException('برنامه یافت نشد');
+ }
+
+ $oldStatus = $application->isActive();
+ $newStatus = !$oldStatus;
+ $application->setIsActive($newStatus);
+
+ $this->entityManager->flush();
+
+ // ثبت لاگ
+ $this->logger->info('OAuth Application Status Changed', [
+ 'application_name' => $application->getName(),
+ 'old_status' => $oldStatus ? 'active' : 'inactive',
+ 'new_status' => $newStatus ? 'active' : 'inactive',
+ 'user_id' => $user->getId(),
+ 'user_email' => $user->getEmail()
+ ]);
+
+ return $this->json($this->extractor->operationSuccess([
+ 'is_active' => $newStatus,
+ 'message' => $newStatus ? 'برنامه فعال شد' : 'برنامه غیرفعال شد'
+ ]));
+ }
+}
\ No newline at end of file
diff --git a/hesabixCore/src/Controller/OAuthController.php b/hesabixCore/src/Controller/OAuthController.php
new file mode 100644
index 0000000..b1fed5d
--- /dev/null
+++ b/hesabixCore/src/Controller/OAuthController.php
@@ -0,0 +1,346 @@
+oauthService = $oauthService;
+ $this->extractor = $extractor;
+ }
+
+ /**
+ * Authorization endpoint - مرحله اول OAuth flow
+ */
+ #[Route('/authorize', name: 'oauth_authorize', methods: ['GET'])]
+ public function authorize(Request $request): Response
+ {
+ try {
+ $validation = $this->oauthService->validateAuthorizationRequest($request);
+ $application = $validation['application'];
+ $scopes = $validation['scopes'];
+ $state = $validation['state'];
+
+ // هدایت به صفحه frontend
+ $frontendUrl = $this->getParameter('app.frontend_url') . '/oauth/authorize?' . $request->getQueryString();
+ return $this->redirect($frontendUrl);
+
+ } catch (AuthenticationException $e) {
+ return new JsonResponse([
+ 'error' => 'invalid_request',
+ 'error_description' => $e->getMessage()
+ ], Response::HTTP_BAD_REQUEST);
+ }
+ }
+
+ /**
+ * API endpoint برای frontend - تایید مجوز
+ */
+ #[Route('/api/oauth/authorize', name: 'api_oauth_authorize', methods: ['POST'])]
+ public function authorizeApi(Request $request): JsonResponse
+ {
+ try {
+ $data = json_decode($request->getContent(), true);
+ $clientId = $data['client_id'] ?? null;
+ $redirectUri = $data['redirect_uri'] ?? null;
+ $scope = $data['scope'] ?? null;
+ $state = $data['state'] ?? null;
+ $approved = $data['approved'] ?? false;
+
+ if (!$this->getUser()) {
+ return $this->json([
+ 'Success' => false,
+ 'message' => 'کاربر احراز هویت نشده'
+ ], Response::HTTP_UNAUTHORIZED);
+ }
+
+ if (!$approved) {
+ // کاربر مجوز را رد کرده
+ $errorParams = [
+ 'error' => 'access_denied',
+ 'error_description' => 'User denied access'
+ ];
+ if ($state) {
+ $errorParams['state'] = $state;
+ }
+
+ $redirectUrl = $redirectUri . '?' . http_build_query($errorParams);
+ return $this->json([
+ 'Success' => true,
+ 'redirect_url' => $redirectUrl
+ ]);
+ }
+
+ $application = $this->oauthService->getApplicationRepository()->findByClientId($clientId);
+ if (!$application) {
+ return $this->json([
+ 'Success' => false,
+ 'message' => 'برنامه نامعتبر'
+ ], Response::HTTP_BAD_REQUEST);
+ }
+
+ $scopes = $scope ? explode(' ', $scope) : [];
+
+ // ایجاد کد مجوز
+ $authorizationCode = $this->oauthService->createAuthorizationCode(
+ $this->getUser(),
+ $application,
+ $scopes,
+ $state
+ );
+
+ // هدایت به redirect_uri با کد مجوز
+ $params = [
+ 'code' => $authorizationCode->getCode()
+ ];
+ if ($state) {
+ $params['state'] = $state;
+ }
+
+ $redirectUrl = $redirectUri . '?' . http_build_query($params);
+ return $this->json([
+ 'Success' => true,
+ 'redirect_url' => $redirectUrl
+ ]);
+
+ } catch (\Exception $e) {
+ $errorParams = [
+ 'error' => 'server_error',
+ 'error_description' => $e->getMessage()
+ ];
+ if ($state) {
+ $errorParams['state'] = $state;
+ }
+
+ $redirectUrl = $redirectUri . '?' . http_build_query($errorParams);
+ return $this->json([
+ 'Success' => true,
+ 'redirect_url' => $redirectUrl
+ ]);
+ }
+ }
+
+ /**
+ * Token endpoint - مرحله دوم OAuth flow
+ */
+ #[Route('/token', name: 'oauth_token', methods: ['POST'])]
+ public function token(Request $request): JsonResponse
+ {
+ $grantType = $request->request->get('grant_type');
+ $clientId = $request->request->get('client_id');
+ $clientSecret = $request->request->get('client_secret');
+
+ // اعتبارسنجی client credentials
+ $application = $this->oauthService->getApplicationRepository()->findByClientId($clientId);
+ if (!$application || $application->getClientSecret() !== $clientSecret) {
+ return $this->json([
+ 'error' => 'invalid_client',
+ 'error_description' => 'Invalid client credentials'
+ ], Response::HTTP_UNAUTHORIZED);
+ }
+
+ switch ($grantType) {
+ case 'authorization_code':
+ return $this->handleAuthorizationCodeGrant($request, $application);
+
+ case 'refresh_token':
+ return $this->handleRefreshTokenGrant($request, $application);
+
+ default:
+ return $this->json([
+ 'error' => 'unsupported_grant_type',
+ 'error_description' => 'Unsupported grant type'
+ ], Response::HTTP_BAD_REQUEST);
+ }
+ }
+
+ /**
+ * مدیریت Authorization Code Grant
+ */
+ private function handleAuthorizationCodeGrant(Request $request, OAuthApplication $application): JsonResponse
+ {
+ $code = $request->request->get('code');
+ $redirectUri = $request->request->get('redirect_uri');
+
+ if (!$code || !$redirectUri) {
+ return $this->json([
+ 'error' => 'invalid_request',
+ 'error_description' => 'Missing required parameters'
+ ], Response::HTTP_BAD_REQUEST);
+ }
+
+ $authorizationCode = $this->oauthService->validateAuthorizationCode($code, $application->getClientId(), $redirectUri);
+ if (!$authorizationCode) {
+ return $this->json([
+ 'error' => 'invalid_grant',
+ 'error_description' => 'Invalid authorization code'
+ ], Response::HTTP_BAD_REQUEST);
+ }
+
+ // استفاده از کد مجوز
+ $this->oauthService->useAuthorizationCode($authorizationCode);
+
+ // ایجاد توکن دسترسی
+ $accessToken = $this->oauthService->createAccessToken(
+ $authorizationCode->getUser(),
+ $application,
+ $authorizationCode->getScopes()
+ );
+
+ return $this->json([
+ 'access_token' => $accessToken->getToken(),
+ 'token_type' => 'Bearer',
+ 'expires_in' => 3600, // 1 hour
+ 'refresh_token' => $accessToken->getRefreshToken(),
+ 'scope' => implode(' ', $accessToken->getScopes())
+ ]);
+ }
+
+ /**
+ * مدیریت Refresh Token Grant
+ */
+ private function handleRefreshTokenGrant(Request $request, OAuthApplication $application): JsonResponse
+ {
+ $refreshToken = $request->request->get('refresh_token');
+
+ if (!$refreshToken) {
+ return $this->json([
+ 'error' => 'invalid_request',
+ 'error_description' => 'Missing refresh_token'
+ ], Response::HTTP_BAD_REQUEST);
+ }
+
+ $accessToken = $this->oauthService->getAccessTokenRepository()->findByRefreshToken($refreshToken);
+ if (!$accessToken || $accessToken->getApplication()->getId() !== $application->getId()) {
+ return $this->json([
+ 'error' => 'invalid_grant',
+ 'error_description' => 'Invalid refresh token'
+ ], Response::HTTP_BAD_REQUEST);
+ }
+
+ // ایجاد توکن جدید
+ $newAccessToken = $this->oauthService->createAccessToken(
+ $accessToken->getUser(),
+ $application,
+ $accessToken->getScopes()
+ );
+
+ // لغو توکن قدیمی
+ $accessToken->setIsRevoked(true);
+ $this->oauthService->getEntityManager()->flush();
+
+ return $this->json([
+ 'access_token' => $newAccessToken->getToken(),
+ 'token_type' => 'Bearer',
+ 'expires_in' => 3600,
+ 'refresh_token' => $newAccessToken->getRefreshToken(),
+ 'scope' => implode(' ', $newAccessToken->getScopes())
+ ]);
+ }
+
+ /**
+ * User Info endpoint
+ */
+ #[Route('/userinfo', name: 'oauth_userinfo', methods: ['GET'])]
+ public function userinfo(Request $request): JsonResponse
+ {
+ $user = $this->getUser();
+ if (!$user) {
+ return $this->json([
+ 'error' => 'invalid_token',
+ 'error_description' => 'Invalid access token'
+ ], Response::HTTP_UNAUTHORIZED);
+ }
+
+ return $this->json([
+ 'id' => $user->getId(),
+ 'email' => $user->getEmail(),
+ 'name' => $user->getName(),
+ 'profile' => [
+ 'phone' => $user->getMobile(),
+ 'address' => $user->getAddress()
+ ]
+ ]);
+ }
+
+ /**
+ * Revoke endpoint
+ */
+ #[Route('/revoke', name: 'oauth_revoke', methods: ['POST'])]
+ public function revoke(Request $request): JsonResponse
+ {
+ $token = $request->request->get('token');
+ $tokenTypeHint = $request->request->get('token_type_hint', 'access_token');
+
+ if (!$token) {
+ return $this->json([
+ 'error' => 'invalid_request',
+ 'error_description' => 'Missing token'
+ ], Response::HTTP_BAD_REQUEST);
+ }
+
+ $success = false;
+ if ($tokenTypeHint === 'access_token') {
+ $success = $this->oauthService->revokeAccessToken($token);
+ } elseif ($tokenTypeHint === 'refresh_token') {
+ $accessToken = $this->oauthService->getAccessTokenRepository()->findByRefreshToken($token);
+ if ($accessToken) {
+ $accessToken->setIsRevoked(true);
+ $this->oauthService->getEntityManager()->flush();
+ $success = true;
+ }
+ }
+
+ return $this->json(['success' => $success]);
+ }
+
+ /**
+ * اطلاعات برنامه OAuth
+ */
+ #[Route('/.well-known/oauth-authorization-server', name: 'oauth_discovery', methods: ['GET'])]
+ public function discovery(): JsonResponse
+ {
+ return $this->json([
+ 'issuer' => $this->getParameter('app.site_url'),
+ 'authorization_endpoint' => $this->generateUrl('oauth_authorize', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL),
+ 'token_endpoint' => $this->generateUrl('oauth_token', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL),
+ 'userinfo_endpoint' => $this->generateUrl('oauth_userinfo', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL),
+ 'revocation_endpoint' => $this->generateUrl('oauth_revoke', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL),
+ 'response_types_supported' => ['code'],
+ 'grant_types_supported' => ['authorization_code', 'refresh_token'],
+ 'token_endpoint_auth_methods_supported' => ['client_secret_post'],
+ 'scopes_supported' => [
+ 'read_profile',
+ 'write_profile',
+ 'read_business',
+ 'write_business',
+ 'read_financial',
+ 'write_financial',
+ 'read_contacts',
+ 'write_contacts',
+ 'read_documents',
+ 'write_documents',
+ 'admin_access'
+ ]
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/hesabixCore/src/Entity/OAuthAccessToken.php b/hesabixCore/src/Entity/OAuthAccessToken.php
new file mode 100644
index 0000000..81ffe83
--- /dev/null
+++ b/hesabixCore/src/Entity/OAuthAccessToken.php
@@ -0,0 +1,216 @@
+createdAt = new \DateTime();
+ $this->lastUsedAt = new \DateTime();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getToken(): ?string
+ {
+ return $this->token;
+ }
+
+ public function setToken(string $token): static
+ {
+ $this->token = $token;
+ return $this;
+ }
+
+ public function getRefreshToken(): ?string
+ {
+ return $this->refreshToken;
+ }
+
+ public function setRefreshToken(?string $refreshToken): static
+ {
+ $this->refreshToken = $refreshToken;
+ return $this;
+ }
+
+ public function getScopes(): array
+ {
+ return $this->scopes;
+ }
+
+ public function setScopes(array $scopes): static
+ {
+ $this->scopes = $scopes;
+ return $this;
+ }
+
+ public function getExpiresAt(): ?\DateTimeInterface
+ {
+ return $this->expiresAt;
+ }
+
+ public function setExpiresAt(\DateTimeInterface $expiresAt): static
+ {
+ $this->expiresAt = $expiresAt;
+ return $this;
+ }
+
+ public function getCreatedAt(): ?\DateTimeInterface
+ {
+ return $this->createdAt;
+ }
+
+ public function setCreatedAt(\DateTimeInterface $createdAt): static
+ {
+ $this->createdAt = $createdAt;
+ return $this;
+ }
+
+ public function getLastUsedAt(): ?\DateTimeInterface
+ {
+ return $this->lastUsedAt;
+ }
+
+ public function setLastUsedAt(?\DateTimeInterface $lastUsedAt): static
+ {
+ $this->lastUsedAt = $lastUsedAt;
+ return $this;
+ }
+
+ public function getUser(): ?User
+ {
+ return $this->user;
+ }
+
+ public function setUser(?User $user): static
+ {
+ $this->user = $user;
+ return $this;
+ }
+
+ public function getApplication(): ?OAuthApplication
+ {
+ return $this->application;
+ }
+
+ public function setApplication(?OAuthApplication $application): static
+ {
+ $this->application = $application;
+ return $this;
+ }
+
+ public function getScope(): ?OAuthScope
+ {
+ return $this->scope;
+ }
+
+ public function setScope(?OAuthScope $scope): static
+ {
+ $this->scope = $scope;
+ return $this;
+ }
+
+ public function isRevoked(): bool
+ {
+ return $this->isRevoked;
+ }
+
+ public function setIsRevoked(bool $isRevoked): static
+ {
+ $this->isRevoked = $isRevoked;
+ return $this;
+ }
+
+ public function getIpAddress(): ?string
+ {
+ return $this->ipAddress;
+ }
+
+ public function setIpAddress(?string $ipAddress): static
+ {
+ $this->ipAddress = $ipAddress;
+ return $this;
+ }
+
+ public function getUserAgent(): ?string
+ {
+ return $this->userAgent;
+ }
+
+ public function setUserAgent(?string $userAgent): static
+ {
+ $this->userAgent = $userAgent;
+ return $this;
+ }
+
+ public function isExpired(): bool
+ {
+ return $this->expiresAt < new \DateTime();
+ }
+
+ public function isValid(): bool
+ {
+ return !$this->isRevoked && !$this->isExpired();
+ }
+
+ public function hasScope(string $scope): bool
+ {
+ return in_array($scope, $this->scopes);
+ }
+
+ public function updateLastUsed(): void
+ {
+ $this->lastUsedAt = new \DateTime();
+ }
+}
\ No newline at end of file
diff --git a/hesabixCore/src/Entity/OAuthApplication.php b/hesabixCore/src/Entity/OAuthApplication.php
new file mode 100644
index 0000000..6960145
--- /dev/null
+++ b/hesabixCore/src/Entity/OAuthApplication.php
@@ -0,0 +1,332 @@
+ 0])]
+ private int $rateLimit = 1000; // تعداد درخواست در ساعت
+
+ #[ORM\Column(type: 'json', nullable: true)]
+ private array $ipWhitelist = [];
+
+ public function __construct()
+ {
+ $this->accessTokens = new ArrayCollection();
+ $this->authorizationCodes = new ArrayCollection();
+ $this->scopes = new ArrayCollection();
+ $this->createdAt = new \DateTime();
+ $this->updatedAt = new \DateTime();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getName(): ?string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): static
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ public function setDescription(?string $description): static
+ {
+ $this->description = $description;
+ return $this;
+ }
+
+ public function getWebsite(): ?string
+ {
+ return $this->website;
+ }
+
+ public function setWebsite(string $website): static
+ {
+ $this->website = $website;
+ return $this;
+ }
+
+ public function getRedirectUri(): ?string
+ {
+ return $this->redirectUri;
+ }
+
+ public function setRedirectUri(string $redirectUri): static
+ {
+ $this->redirectUri = $redirectUri;
+ return $this;
+ }
+
+ public function getClientId(): ?string
+ {
+ return $this->clientId;
+ }
+
+ public function setClientId(string $clientId): static
+ {
+ $this->clientId = $clientId;
+ return $this;
+ }
+
+ public function getClientSecret(): ?string
+ {
+ return $this->clientSecret;
+ }
+
+ public function setClientSecret(string $clientSecret): static
+ {
+ $this->clientSecret = $clientSecret;
+ return $this;
+ }
+
+ public function isActive(): bool
+ {
+ return $this->isActive;
+ }
+
+ public function setIsActive(bool $isActive): static
+ {
+ $this->isActive = $isActive;
+ return $this;
+ }
+
+
+
+ public function getLogoUrl(): ?string
+ {
+ return $this->logoUrl;
+ }
+
+ public function setLogoUrl(?string $logoUrl): static
+ {
+ $this->logoUrl = $logoUrl;
+ return $this;
+ }
+
+ public function getCreatedAt(): ?\DateTimeInterface
+ {
+ return $this->createdAt;
+ }
+
+ public function setCreatedAt(\DateTimeInterface $createdAt): static
+ {
+ $this->createdAt = $createdAt;
+ return $this;
+ }
+
+ public function getUpdatedAt(): ?\DateTimeInterface
+ {
+ return $this->updatedAt;
+ }
+
+ public function setUpdatedAt(\DateTimeInterface $updatedAt): static
+ {
+ $this->updatedAt = $updatedAt;
+ return $this;
+ }
+
+ public function getOwner(): ?User
+ {
+ return $this->owner;
+ }
+
+ public function setOwner(?User $owner): static
+ {
+ $this->owner = $owner;
+ return $this;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getAccessTokens(): Collection
+ {
+ return $this->accessTokens;
+ }
+
+ public function addAccessToken(OAuthAccessToken $accessToken): static
+ {
+ if (!$this->accessTokens->contains($accessToken)) {
+ $this->accessTokens->add($accessToken);
+ $accessToken->setApplication($this);
+ }
+ return $this;
+ }
+
+ public function removeAccessToken(OAuthAccessToken $accessToken): static
+ {
+ if ($this->accessTokens->removeElement($accessToken)) {
+ if ($accessToken->getApplication() === $this) {
+ $accessToken->setApplication(null);
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getAuthorizationCodes(): Collection
+ {
+ return $this->authorizationCodes;
+ }
+
+ public function addAuthorizationCode(OAuthAuthorizationCode $authorizationCode): static
+ {
+ if (!$this->authorizationCodes->contains($authorizationCode)) {
+ $this->authorizationCodes->add($authorizationCode);
+ $authorizationCode->setApplication($this);
+ }
+ return $this;
+ }
+
+ public function removeAuthorizationCode(OAuthAuthorizationCode $authorizationCode): static
+ {
+ if ($this->authorizationCodes->removeElement($authorizationCode)) {
+ if ($authorizationCode->getApplication() === $this) {
+ $authorizationCode->setApplication(null);
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getScopes(): Collection
+ {
+ return $this->scopes;
+ }
+
+ public function addScope(OAuthScope $scope): static
+ {
+ if (!$this->scopes->contains($scope)) {
+ $this->scopes->add($scope);
+ }
+ return $this;
+ }
+
+ public function removeScope(OAuthScope $scope): static
+ {
+ $this->scopes->removeElement($scope);
+ return $this;
+ }
+
+ public function getAllowedScopes(): array
+ {
+ return $this->allowedScopes;
+ }
+
+ public function setAllowedScopes(array $allowedScopes): static
+ {
+ $this->allowedScopes = $allowedScopes;
+ return $this;
+ }
+
+ public function getRateLimit(): int
+ {
+ return $this->rateLimit;
+ }
+
+ public function setRateLimit(int $rateLimit): static
+ {
+ $this->rateLimit = $rateLimit;
+ return $this;
+ }
+
+ public function getIpWhitelist(): array
+ {
+ return $this->ipWhitelist;
+ }
+
+ public function setIpWhitelist(array $ipWhitelist): static
+ {
+ $this->ipWhitelist = $ipWhitelist;
+ return $this;
+ }
+
+ #[ORM\PreUpdate]
+ public function setUpdatedAtValue(): void
+ {
+ $this->updatedAt = new \DateTime();
+ }
+}
\ No newline at end of file
diff --git a/hesabixCore/src/Entity/OAuthAuthorizationCode.php b/hesabixCore/src/Entity/OAuthAuthorizationCode.php
new file mode 100644
index 0000000..08e1035
--- /dev/null
+++ b/hesabixCore/src/Entity/OAuthAuthorizationCode.php
@@ -0,0 +1,192 @@
+createdAt = new \DateTime();
+ $this->expiresAt = (new \DateTime())->modify('+10 minutes');
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getCode(): ?string
+ {
+ return $this->code;
+ }
+
+ public function setCode(string $code): static
+ {
+ $this->code = $code;
+ return $this;
+ }
+
+ public function getRedirectUri(): ?string
+ {
+ return $this->redirectUri;
+ }
+
+ public function setRedirectUri(string $redirectUri): static
+ {
+ $this->redirectUri = $redirectUri;
+ return $this;
+ }
+
+ public function getScopes(): array
+ {
+ return $this->scopes;
+ }
+
+ public function setScopes(array $scopes): static
+ {
+ $this->scopes = $scopes;
+ return $this;
+ }
+
+ public function getExpiresAt(): ?\DateTimeInterface
+ {
+ return $this->expiresAt;
+ }
+
+ public function setExpiresAt(\DateTimeInterface $expiresAt): static
+ {
+ $this->expiresAt = $expiresAt;
+ return $this;
+ }
+
+ public function isUsed(): bool
+ {
+ return $this->isUsed;
+ }
+
+ public function setIsUsed(bool $isUsed): static
+ {
+ $this->isUsed = $isUsed;
+ return $this;
+ }
+
+ public function getCreatedAt(): ?\DateTimeInterface
+ {
+ return $this->createdAt;
+ }
+
+ public function setCreatedAt(\DateTimeInterface $createdAt): static
+ {
+ $this->createdAt = $createdAt;
+ return $this;
+ }
+
+ public function getUser(): ?User
+ {
+ return $this->user;
+ }
+
+ public function setUser(?User $user): static
+ {
+ $this->user = $user;
+ return $this;
+ }
+
+ public function getApplication(): ?OAuthApplication
+ {
+ return $this->application;
+ }
+
+ public function setApplication(?OAuthApplication $application): static
+ {
+ $this->application = $application;
+ return $this;
+ }
+
+ public function getState(): ?string
+ {
+ return $this->state;
+ }
+
+ public function setState(?string $state): static
+ {
+ $this->state = $state;
+ return $this;
+ }
+
+ public function getCodeChallenge(): ?string
+ {
+ return $this->codeChallenge;
+ }
+
+ public function setCodeChallenge(?string $codeChallenge): static
+ {
+ $this->codeChallenge = $codeChallenge;
+ return $this;
+ }
+
+ public function getCodeChallengeMethod(): ?string
+ {
+ return $this->codeChallengeMethod;
+ }
+
+ public function setCodeChallengeMethod(?string $codeChallengeMethod): static
+ {
+ $this->codeChallengeMethod = $codeChallengeMethod;
+ return $this;
+ }
+
+ public function isExpired(): bool
+ {
+ return $this->expiresAt < new \DateTime();
+ }
+
+ public function isValid(): bool
+ {
+ return !$this->isUsed && !$this->isExpired();
+ }
+}
\ No newline at end of file
diff --git a/hesabixCore/src/Entity/OAuthScope.php b/hesabixCore/src/Entity/OAuthScope.php
new file mode 100644
index 0000000..9451d0b
--- /dev/null
+++ b/hesabixCore/src/Entity/OAuthScope.php
@@ -0,0 +1,162 @@
+applications = new ArrayCollection();
+ $this->accessTokens = new ArrayCollection();
+ $this->createdAt = new \DateTime();
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getName(): ?string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): static
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ public function setDescription(string $description): static
+ {
+ $this->description = $description;
+ return $this;
+ }
+
+ public function isDefault(): bool
+ {
+ return $this->isDefault;
+ }
+
+ public function setIsDefault(bool $isDefault): static
+ {
+ $this->isDefault = $isDefault;
+ return $this;
+ }
+
+ public function isSystem(): bool
+ {
+ return $this->isSystem;
+ }
+
+ public function setIsSystem(bool $isSystem): static
+ {
+ $this->isSystem = $isSystem;
+ return $this;
+ }
+
+ public function getCreatedAt(): ?\DateTimeInterface
+ {
+ return $this->createdAt;
+ }
+
+ public function setCreatedAt(\DateTimeInterface $createdAt): static
+ {
+ $this->createdAt = $createdAt;
+ return $this;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getApplications(): Collection
+ {
+ return $this->applications;
+ }
+
+ public function addApplication(OAuthApplication $application): static
+ {
+ if (!$this->applications->contains($application)) {
+ $this->applications->add($application);
+ $application->addScope($this);
+ }
+ return $this;
+ }
+
+ public function removeApplication(OAuthApplication $application): static
+ {
+ if ($this->applications->removeElement($application)) {
+ $application->removeScope($this);
+ }
+ return $this;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getAccessTokens(): Collection
+ {
+ return $this->accessTokens;
+ }
+
+ public function addAccessToken(OAuthAccessToken $accessToken): static
+ {
+ if (!$this->accessTokens->contains($accessToken)) {
+ $this->accessTokens->add($accessToken);
+ $accessToken->setScope($this);
+ }
+ return $this;
+ }
+
+ public function removeAccessToken(OAuthAccessToken $accessToken): static
+ {
+ if ($this->accessTokens->removeElement($accessToken)) {
+ if ($accessToken->getScope() === $this) {
+ $accessToken->setScope(null);
+ }
+ }
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/hesabixCore/src/Repository/OAuthAccessTokenRepository.php b/hesabixCore/src/Repository/OAuthAccessTokenRepository.php
new file mode 100644
index 0000000..4371c2a
--- /dev/null
+++ b/hesabixCore/src/Repository/OAuthAccessTokenRepository.php
@@ -0,0 +1,144 @@
+
+ *
+ * @method OAuthAccessToken|null find($id, $lockMode = null, $lockVersion = null)
+ * @method OAuthAccessToken|null findOneBy(array $criteria, array $orderBy = null)
+ * @method OAuthAccessToken[] findAll()
+ * @method OAuthAccessToken[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class OAuthAccessTokenRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, OAuthAccessToken::class);
+ }
+
+ public function save(OAuthAccessToken $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->persist($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function remove(OAuthAccessToken $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->remove($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function findByToken(string $token): ?OAuthAccessToken
+ {
+ return $this->findOneBy(['token' => $token]);
+ }
+
+ public function findValidByToken(string $token): ?OAuthAccessToken
+ {
+ $qb = $this->createQueryBuilder('at')
+ ->andWhere('at.token = :token')
+ ->andWhere('at.isRevoked = :isRevoked')
+ ->andWhere('at.expiresAt > :now')
+ ->setParameter('token', $token)
+ ->setParameter('isRevoked', false)
+ ->setParameter('now', new \DateTime());
+
+ return $qb->getQuery()->getOneOrNullResult();
+ }
+
+ public function findByRefreshToken(string $refreshToken): ?OAuthAccessToken
+ {
+ return $this->findOneBy(['refreshToken' => $refreshToken]);
+ }
+
+ public function findValidByRefreshToken(string $refreshToken): ?OAuthAccessToken
+ {
+ $qb = $this->createQueryBuilder('at')
+ ->andWhere('at.refreshToken = :refreshToken')
+ ->andWhere('at.isRevoked = :isRevoked')
+ ->andWhere('at.expiresAt > :now')
+ ->setParameter('refreshToken', $refreshToken)
+ ->setParameter('isRevoked', false)
+ ->setParameter('now', new \DateTime());
+
+ return $qb->getQuery()->getOneOrNullResult();
+ }
+
+ public function findByUser(int $userId): array
+ {
+ return $this->createQueryBuilder('at')
+ ->andWhere('at.user = :userId')
+ ->setParameter('userId', $userId)
+ ->orderBy('at.createdAt', 'DESC')
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function findByApplication(int $applicationId): array
+ {
+ return $this->createQueryBuilder('at')
+ ->andWhere('at.application = :applicationId')
+ ->setParameter('applicationId', $applicationId)
+ ->orderBy('at.createdAt', 'DESC')
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function findExpiredTokens(): array
+ {
+ return $this->createQueryBuilder('at')
+ ->andWhere('at.expiresAt < :now')
+ ->setParameter('now', new \DateTime())
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function cleanupExpiredTokens(): int
+ {
+ $qb = $this->createQueryBuilder('at')
+ ->delete()
+ ->andWhere('at.expiresAt < :now')
+ ->setParameter('now', new \DateTime());
+
+ return $qb->getQuery()->execute();
+ }
+
+ public function revokeUserTokens(int $userId): int
+ {
+ $qb = $this->createQueryBuilder('at')
+ ->update()
+ ->set('at.isRevoked', ':isRevoked')
+ ->andWhere('at.user = :userId')
+ ->andWhere('at.isRevoked = :currentRevoked')
+ ->setParameter('isRevoked', true)
+ ->setParameter('userId', $userId)
+ ->setParameter('currentRevoked', false);
+
+ return $qb->getQuery()->execute();
+ }
+
+ public function revokeApplicationTokens(int $applicationId): int
+ {
+ $qb = $this->createQueryBuilder('at')
+ ->update()
+ ->set('at.isRevoked', ':isRevoked')
+ ->andWhere('at.application = :applicationId')
+ ->andWhere('at.isRevoked = :currentRevoked')
+ ->setParameter('isRevoked', true)
+ ->setParameter('applicationId', $applicationId)
+ ->setParameter('currentRevoked', false);
+
+ return $qb->getQuery()->execute();
+ }
+}
\ No newline at end of file
diff --git a/hesabixCore/src/Repository/OAuthApplicationRepository.php b/hesabixCore/src/Repository/OAuthApplicationRepository.php
new file mode 100644
index 0000000..ffc899e
--- /dev/null
+++ b/hesabixCore/src/Repository/OAuthApplicationRepository.php
@@ -0,0 +1,68 @@
+
+ *
+ * @method OAuthApplication|null find($id, $lockMode = null, $lockVersion = null)
+ * @method OAuthApplication|null findOneBy(array $criteria, array $orderBy = null)
+ * @method OAuthApplication[] findAll()
+ * @method OAuthApplication[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class OAuthApplicationRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, OAuthApplication::class);
+ }
+
+ public function save(OAuthApplication $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->persist($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function remove(OAuthApplication $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->remove($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function findByClientId(string $clientId): ?OAuthApplication
+ {
+ return $this->findOneBy(['clientId' => $clientId, 'isActive' => true]);
+ }
+
+ public function findByOwner(int $ownerId): array
+ {
+ return $this->createQueryBuilder('o')
+ ->andWhere('o.owner = :ownerId')
+ ->setParameter('ownerId', $ownerId)
+ ->orderBy('o.createdAt', 'DESC')
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function findActiveApplications(): array
+ {
+ return $this->createQueryBuilder('o')
+ ->andWhere('o.isActive = :active')
+ ->setParameter('active', true)
+ ->orderBy('o.name', 'ASC')
+ ->getQuery()
+ ->getResult();
+ }
+
+
+}
\ No newline at end of file
diff --git a/hesabixCore/src/Repository/OAuthAuthorizationCodeRepository.php b/hesabixCore/src/Repository/OAuthAuthorizationCodeRepository.php
new file mode 100644
index 0000000..345e779
--- /dev/null
+++ b/hesabixCore/src/Repository/OAuthAuthorizationCodeRepository.php
@@ -0,0 +1,78 @@
+
+ *
+ * @method OAuthAuthorizationCode|null find($id, $lockMode = null, $lockVersion = null)
+ * @method OAuthAuthorizationCode|null findOneBy(array $criteria, array $orderBy = null)
+ * @method OAuthAuthorizationCode[] findAll()
+ * @method OAuthAuthorizationCode[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class OAuthAuthorizationCodeRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, OAuthAuthorizationCode::class);
+ }
+
+ public function save(OAuthAuthorizationCode $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->persist($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function remove(OAuthAuthorizationCode $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->remove($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function findByCode(string $code): ?OAuthAuthorizationCode
+ {
+ return $this->findOneBy(['code' => $code]);
+ }
+
+ public function findValidByCode(string $code): ?OAuthAuthorizationCode
+ {
+ $qb = $this->createQueryBuilder('ac')
+ ->andWhere('ac.code = :code')
+ ->andWhere('ac.isUsed = :isUsed')
+ ->andWhere('ac.expiresAt > :now')
+ ->setParameter('code', $code)
+ ->setParameter('isUsed', false)
+ ->setParameter('now', new \DateTime());
+
+ return $qb->getQuery()->getOneOrNullResult();
+ }
+
+ public function findExpiredCodes(): array
+ {
+ return $this->createQueryBuilder('ac')
+ ->andWhere('ac.expiresAt < :now')
+ ->setParameter('now', new \DateTime())
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function cleanupExpiredCodes(): int
+ {
+ $qb = $this->createQueryBuilder('ac')
+ ->delete()
+ ->andWhere('ac.expiresAt < :now')
+ ->setParameter('now', new \DateTime());
+
+ return $qb->getQuery()->execute();
+ }
+}
\ No newline at end of file
diff --git a/hesabixCore/src/Repository/OAuthScopeRepository.php b/hesabixCore/src/Repository/OAuthScopeRepository.php
new file mode 100644
index 0000000..e87d89f
--- /dev/null
+++ b/hesabixCore/src/Repository/OAuthScopeRepository.php
@@ -0,0 +1,75 @@
+
+ *
+ * @method OAuthScope|null find($id, $lockMode = null, $lockVersion = null)
+ * @method OAuthScope|null findOneBy(array $criteria, array $orderBy = null)
+ * @method OAuthScope[] findAll()
+ * @method OAuthScope[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class OAuthScopeRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, OAuthScope::class);
+ }
+
+ public function save(OAuthScope $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->persist($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function remove(OAuthScope $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->remove($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function findByName(string $name): ?OAuthScope
+ {
+ return $this->findOneBy(['name' => $name]);
+ }
+
+ public function findDefaultScopes(): array
+ {
+ return $this->createQueryBuilder('s')
+ ->andWhere('s.isDefault = :isDefault')
+ ->setParameter('isDefault', true)
+ ->orderBy('s.name', 'ASC')
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function findSystemScopes(): array
+ {
+ return $this->createQueryBuilder('s')
+ ->andWhere('s.isSystem = :isSystem')
+ ->setParameter('isSystem', true)
+ ->orderBy('s.name', 'ASC')
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function findByNames(array $names): array
+ {
+ return $this->createQueryBuilder('s')
+ ->andWhere('s.name IN (:names)')
+ ->setParameter('names', $names)
+ ->getQuery()
+ ->getResult();
+ }
+}
\ No newline at end of file
diff --git a/hesabixCore/src/Security/OAuthAuthenticator.php b/hesabixCore/src/Security/OAuthAuthenticator.php
new file mode 100644
index 0000000..7059ab1
--- /dev/null
+++ b/hesabixCore/src/Security/OAuthAuthenticator.php
@@ -0,0 +1,75 @@
+oauthService = $oauthService;
+ }
+
+ /**
+ * Called on every request to decide if this authenticator should be
+ * used for the request. Returning `false` will cause this authenticator
+ * to be skipped.
+ */
+ public function supports(Request $request): ?bool
+ {
+ $authorization = $request->headers->get('Authorization');
+ return $authorization && str_starts_with($authorization, 'Bearer ');
+ }
+
+ public function authenticate(Request $request): Passport
+ {
+ $authorization = $request->headers->get('Authorization');
+ $token = substr($authorization, 7);
+
+ if (empty($token)) {
+ throw new CustomUserMessageAuthenticationException('No Bearer token provided');
+ }
+
+ return new SelfValidatingPassport(
+ new UserBadge($token, function($token) {
+ $accessToken = $this->oauthService->validateAccessToken($token);
+ if (!$accessToken) {
+ throw new UserNotFoundException('Invalid access token');
+ }
+ return $accessToken->getUser();
+ })
+ );
+ }
+
+ public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
+ {
+ // on success, let the request continue
+ return null;
+ }
+
+ public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
+ {
+ $data = [
+ 'error' => 'invalid_token',
+ 'error_description' => $exception->getMessage()
+ ];
+
+ return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
+ }
+}
\ No newline at end of file
diff --git a/hesabixCore/src/Service/OAuthService.php b/hesabixCore/src/Service/OAuthService.php
new file mode 100644
index 0000000..eac2dd3
--- /dev/null
+++ b/hesabixCore/src/Service/OAuthService.php
@@ -0,0 +1,303 @@
+entityManager = $entityManager;
+ $this->applicationRepository = $applicationRepository;
+ $this->authorizationCodeRepository = $authorizationCodeRepository;
+ $this->accessTokenRepository = $accessTokenRepository;
+ $this->scopeRepository = $scopeRepository;
+ }
+
+ /**
+ * تولید client_id و client_secret برای برنامه جدید
+ */
+ public function generateClientCredentials(): array
+ {
+ $clientId = $this->generateRandomString(32);
+ $clientSecret = $this->generateRandomString(64);
+
+ return [
+ 'client_id' => $clientId,
+ 'client_secret' => $clientSecret
+ ];
+ }
+
+ /**
+ * اعتبارسنجی درخواست authorization
+ */
+ public function validateAuthorizationRequest(Request $request): array
+ {
+ $clientId = $request->query->get('client_id');
+ $redirectUri = $request->query->get('redirect_uri');
+ $responseType = $request->query->get('response_type');
+ $scope = $request->query->get('scope');
+ $state = $request->query->get('state');
+
+ // بررسی وجود پارامترهای اجباری
+ if (!$clientId || !$redirectUri || !$responseType) {
+ throw new AuthenticationException('Missing required parameters');
+ }
+
+ // بررسی نوع response
+ if ($responseType !== 'code') {
+ throw new AuthenticationException('Unsupported response_type');
+ }
+
+ // بررسی وجود برنامه
+ $application = $this->applicationRepository->findByClientId($clientId);
+ if (!$application) {
+ throw new AuthenticationException('Invalid client_id');
+ }
+
+ // بررسی فعال بودن برنامه
+ if (!$application->isActive()) {
+ throw new AuthenticationException('Application is not active');
+ }
+
+ // بررسی redirect_uri
+ if ($application->getRedirectUri() !== $redirectUri) {
+ throw new AuthenticationException('Invalid redirect_uri');
+ }
+
+ // بررسی محدودههای دسترسی
+ $requestedScopes = $scope ? explode(' ', $scope) : [];
+ $allowedScopes = $application->getAllowedScopes();
+
+ $validScopes = array_intersect($requestedScopes, $allowedScopes);
+ if (empty($validScopes) && !empty($requestedScopes)) {
+ throw new AuthenticationException('Invalid scope');
+ }
+
+ return [
+ 'application' => $application,
+ 'scopes' => $validScopes,
+ 'state' => $state
+ ];
+ }
+
+ /**
+ * ایجاد کد مجوز
+ */
+ public function createAuthorizationCode(User $user, OAuthApplication $application, array $scopes, ?string $state = null): OAuthAuthorizationCode
+ {
+ $code = new OAuthAuthorizationCode();
+ $code->setCode($this->generateRandomString(64));
+ $code->setUser($user);
+ $code->setApplication($application);
+ $code->setRedirectUri($application->getRedirectUri());
+ $code->setScopes($scopes);
+ $code->setState($state);
+ $code->setExpiresAt((new \DateTime())->modify('+10 minutes'));
+
+ $this->entityManager->persist($code);
+ $this->entityManager->flush();
+
+ return $code;
+ }
+
+ /**
+ * اعتبارسنجی کد مجوز
+ */
+ public function validateAuthorizationCode(string $code, string $clientId, string $redirectUri): ?OAuthAuthorizationCode
+ {
+ $authorizationCode = $this->authorizationCodeRepository->findValidByCode($code);
+
+ if (!$authorizationCode) {
+ return null;
+ }
+
+ $application = $authorizationCode->getApplication();
+
+ // بررسی تطابق client_id
+ if ($application->getClientId() !== $clientId) {
+ return null;
+ }
+
+ // بررسی تطابق redirect_uri
+ if ($authorizationCode->getRedirectUri() !== $redirectUri) {
+ return null;
+ }
+
+ return $authorizationCode;
+ }
+
+ /**
+ * استفاده از کد مجوز
+ */
+ public function useAuthorizationCode(OAuthAuthorizationCode $authorizationCode): void
+ {
+ $authorizationCode->setIsUsed(true);
+ $this->entityManager->flush();
+ }
+
+ /**
+ * ایجاد توکن دسترسی
+ */
+ public function createAccessToken(User $user, OAuthApplication $application, array $scopes): OAuthAccessToken
+ {
+ $token = new OAuthAccessToken();
+ $token->setToken($this->generateRandomString(64));
+ $token->setRefreshToken($this->generateRandomString(64));
+ $token->setUser($user);
+ $token->setApplication($application);
+ $token->setScopes($scopes);
+ $token->setExpiresAt((new \DateTime())->modify('+1 hour'));
+
+ $this->entityManager->persist($token);
+ $this->entityManager->flush();
+
+ return $token;
+ }
+
+ /**
+ * تمدید توکن دسترسی
+ */
+ public function refreshAccessToken(string $refreshToken): ?OAuthAccessToken
+ {
+ $oldToken = $this->accessTokenRepository->findValidByRefreshToken($refreshToken);
+
+ if (!$oldToken) {
+ return null;
+ }
+
+ // ایجاد توکن جدید
+ $newToken = new OAuthAccessToken();
+ $newToken->setToken($this->generateRandomString(64));
+ $newToken->setRefreshToken($this->generateRandomString(64));
+ $newToken->setUser($oldToken->getUser());
+ $newToken->setApplication($oldToken->getApplication());
+ $newToken->setScopes($oldToken->getScopes());
+ $newToken->setExpiresAt((new \DateTime())->modify('+1 hour'));
+
+ // لغو توکن قدیمی
+ $oldToken->setIsRevoked(true);
+
+ $this->entityManager->persist($newToken);
+ $this->entityManager->flush();
+
+ return $newToken;
+ }
+
+ /**
+ * اعتبارسنجی توکن دسترسی
+ */
+ public function validateAccessToken(string $token): ?OAuthAccessToken
+ {
+ $accessToken = $this->accessTokenRepository->findValidByToken($token);
+
+ if ($accessToken) {
+ $accessToken->updateLastUsed();
+ $this->entityManager->flush();
+ }
+
+ return $accessToken;
+ }
+
+ /**
+ * لغو توکن دسترسی
+ */
+ public function revokeAccessToken(string $token): bool
+ {
+ $accessToken = $this->accessTokenRepository->findByToken($token);
+
+ if (!$accessToken) {
+ return false;
+ }
+
+ $accessToken->setIsRevoked(true);
+ $this->entityManager->flush();
+
+ return true;
+ }
+
+ /**
+ * ایجاد محدودههای پیشفرض سیستم
+ */
+ public function createDefaultScopes(): void
+ {
+ $defaultScopes = [
+ 'read_profile' => 'دسترسی به اطلاعات پروفایل کاربر',
+ 'write_profile' => 'ویرایش اطلاعات پروفایل کاربر',
+ 'read_business' => 'دسترسی به اطلاعات کسبوکار',
+ 'write_business' => 'ویرایش اطلاعات کسبوکار',
+ 'read_accounting' => 'دسترسی به اطلاعات حسابداری',
+ 'write_accounting' => 'ویرایش اطلاعات حسابداری',
+ 'read_reports' => 'دسترسی به گزارشها',
+ 'write_reports' => 'ایجاد و ویرایش گزارشها',
+ 'admin' => 'دسترسی مدیریتی کامل'
+ ];
+
+ foreach ($defaultScopes as $name => $description) {
+ $existingScope = $this->scopeRepository->findByName($name);
+ if (!$existingScope) {
+ $scope = new OAuthScope();
+ $scope->setName($name);
+ $scope->setDescription($description);
+ $scope->setIsSystem(true);
+ $scope->setIsDefault(in_array($name, ['read_profile', 'read_business']));
+
+ $this->entityManager->persist($scope);
+ }
+ }
+
+ $this->entityManager->flush();
+ }
+
+ /**
+ * تولید رشته تصادفی
+ */
+ private function generateRandomString(int $length): string
+ {
+ $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ $string = '';
+
+ for ($i = 0; $i < $length; $i++) {
+ $string .= $characters[random_int(0, strlen($characters) - 1)];
+ }
+
+ return $string;
+ }
+
+ /**
+ * پاکسازی کدها و توکنهای منقضی شده
+ */
+ public function cleanupExpiredItems(): array
+ {
+ $expiredCodes = $this->authorizationCodeRepository->cleanupExpiredCodes();
+ $expiredTokens = $this->accessTokenRepository->cleanupExpiredTokens();
+
+ return [
+ 'expired_codes' => $expiredCodes,
+ 'expired_tokens' => $expiredTokens
+ ];
+ }
+}
\ No newline at end of file
diff --git a/webUI/src/components/OAuthManager.vue b/webUI/src/components/OAuthManager.vue
new file mode 100644
index 0000000..fbd8405
--- /dev/null
+++ b/webUI/src/components/OAuthManager.vue
@@ -0,0 +1,577 @@
+
+
+
+
+
+ mdi-oauth
+
+
برنامههای OAuth
+
مدیریت برنامههای احراز هویت خارجی
+
+
+
+
+
+
+
+ mdi-information
+
+
+ OAuth چیست؟
+ OAuth یک پروتکل استاندارد برای احراز هویت است که به برنامههای خارجی اجازه میدهد
+ بدون نیاز به رمز عبور، به حساب کاربران دسترسی داشته باشند. این روش امنتر و راحتتر از
+ روشهای سنتی است.
+
+
+
+
+
+
+
+ mdi-oauth
+ مدیریت برنامهها
+
+
+ برنامه جدید
+
+
+
+
+
+ هنوز هیچ برنامه OAuth ایجاد نکردهاید. برای شروع، یک برنامه جدید ایجاد کنید.
+
+
+
+
+
+
+
+ OAuth یک پروتکل استاندارد برای احراز هویت است که به برنامههای خارجی اجازه میدهد
+ بدون نیاز به رمز عبور، به حساب کاربران دسترسی داشته باشند. این روش امنتر و راحتتر از
+ روشهای سنتی است.
+
+
+
+
+
+
+
+
+
برنامههای شما
+
+ {{ oauthApplications.length }} برنامه
+
+
+
+
+ mdi-application-outline
+
+ هنوز برنامهای ایجاد نکردهاید
+
+
+ برای شروع استفاده از OAuth، اولین برنامه خود را ایجاد کنید
+
+ در صورت خالی بودن، از هر آدرس IP میتوان به برنامه متصل شد.
+ برای محدود کردن دسترسی، آدرسهای IP مجاز را وارد کنید.
+
+
+
+
+
+ افزودن
+
+
+
+
+
+ {{ ip }}
+
+
+
+
+
+ mdi-information
+
+ هیچ آدرس IP محدودیتی تعریف نشده است. از هر آدرس IP میتوان به برنامه متصل شد.
+
+
+
+
+
+
+
+
+
+ mdi-shield
+ محدودههای دسترسی (Scopes)
+
+
+
+ محدودههای دسترسی که این برنامه میتواند از کاربران درخواست کند.
+
+
+
+
+
+
+
+
{{ scope.name }}
+
{{ scope.description }}
+
+
+
+
+
+
+
+
+ mdi-alert
+
+ هیچ محدوده دسترسی انتخاب نشده است. این برنامه دسترسی محدودی خواهد داشت.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ انصراف
+
+
+ {{ editingOAuthApp ? 'ویرایش' : 'ایجاد' }}
+
+
+
+
+
+
+
+ {{ oauthSnackbar.text }}
+
+
+
+ بستن
+
+
+
\ No newline at end of file