add first release for oauth application syncing
This commit is contained in:
parent
51d68b9874
commit
da644e3260
189
docs/OAuth_Cleanup_Summary.md
Normal file
189
docs/OAuth_Cleanup_Summary.md
Normal file
|
@ -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
|
957
docs/OAuth_Complete_Documentation.md
Normal file
957
docs/OAuth_Complete_Documentation.md
Normal file
|
@ -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
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OAuth Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<button onclick="login()">ورود با Hesabix</button>
|
||||
|
||||
<script>
|
||||
const CLIENT_ID = 'YOUR_CLIENT_ID';
|
||||
const REDIRECT_URI = 'https://myapp.com/callback';
|
||||
|
||||
function login() {
|
||||
const params = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: CLIENT_ID,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
scope: 'read_profile',
|
||||
state: Math.random().toString(36)
|
||||
});
|
||||
|
||||
window.location.href = `https://hesabix.com/oauth/authorize?${params}`;
|
||||
}
|
||||
|
||||
// بررسی callback
|
||||
if (window.location.search.includes('code=')) {
|
||||
handleCallback();
|
||||
}
|
||||
|
||||
async function handleCallback() {
|
||||
const code = new URLSearchParams(window.location.search).get('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: CLIENT_ID,
|
||||
client_secret: 'YOUR_CLIENT_SECRET',
|
||||
code: code,
|
||||
redirect_uri: REDIRECT_URI
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Token received:', data);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### مثال 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
|
199
docs/OAuth_Copy_Issue_Fix.md
Normal file
199
docs/OAuth_Copy_Issue_Fix.md
Normal file
|
@ -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
|
181
docs/OAuth_Frontend_Integration.md
Normal file
181
docs/OAuth_Frontend_Integration.md
Normal file
|
@ -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
|
331
docs/OAuth_README.md
Normal file
331
docs/OAuth_README.md
Normal file
|
@ -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 تماس بگیرید.
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
107
hesabixCore/migrations/Version20250815230230.php
Normal file
107
hesabixCore/migrations/Version20250815230230.php
Normal file
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250815230230 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->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);
|
||||
}
|
||||
}
|
35
hesabixCore/migrations/Version20250816003509.php
Normal file
35
hesabixCore/migrations/Version20250816003509.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250816003509 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->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);
|
||||
}
|
||||
}
|
64
hesabixCore/src/Command/CleanupOAuthCommand.php
Normal file
64
hesabixCore/src/Command/CleanupOAuthCommand.php
Normal file
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Service\OAuthService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:cleanup-oauth',
|
||||
description: 'پاکسازی توکنها و کدهای منقضی شده OAuth',
|
||||
)]
|
||||
class CleanupOAuthCommand extends Command
|
||||
{
|
||||
private OAuthService $oauthService;
|
||||
|
||||
public function __construct(OAuthService $oauthService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
}
|
64
hesabixCore/src/Command/CreateOAuthScopesCommand.php
Normal file
64
hesabixCore/src/Command/CreateOAuthScopesCommand.php
Normal file
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Service\OAuthService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:create-oauth-scopes',
|
||||
description: 'ایجاد محدودههای پیشفرض OAuth',
|
||||
)]
|
||||
class CreateOAuthScopesCommand extends Command
|
||||
{
|
||||
private OAuthService $oauthService;
|
||||
|
||||
public function __construct(OAuthService $oauthService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
}
|
453
hesabixCore/src/Controller/OAuthApplicationController.php
Normal file
453
hesabixCore/src/Controller/OAuthApplicationController.php
Normal file
|
@ -0,0 +1,453 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\OAuthApplication;
|
||||
use App\Entity\OAuthScope;
|
||||
use App\Service\OAuthService;
|
||||
use App\Service\Extractor;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use App\Service\Provider;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
#[Route('/api/admin/oauth')]
|
||||
class OAuthApplicationController extends AbstractController
|
||||
{
|
||||
private OAuthService $oauthService;
|
||||
private Extractor $extractor;
|
||||
private LoggerInterface $logger;
|
||||
private Provider $provider;
|
||||
private EntityManagerInterface $entityManager;
|
||||
private ValidatorInterface $validator;
|
||||
|
||||
public function __construct(
|
||||
OAuthService $oauthService,
|
||||
Extractor $extractor,
|
||||
#[Autowire('@monolog.logger.oauth')] LoggerInterface $logger,
|
||||
Provider $provider,
|
||||
EntityManagerInterface $entityManager,
|
||||
ValidatorInterface $validator
|
||||
) {
|
||||
$this->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 ? 'برنامه فعال شد' : 'برنامه غیرفعال شد'
|
||||
]));
|
||||
}
|
||||
}
|
346
hesabixCore/src/Controller/OAuthController.php
Normal file
346
hesabixCore/src/Controller/OAuthController.php
Normal file
|
@ -0,0 +1,346 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\OAuthApplication;
|
||||
use App\Entity\OAuthAuthorizationCode;
|
||||
use App\Entity\OAuthAccessToken;
|
||||
use App\Service\OAuthService;
|
||||
use App\Service\Extractor;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
#[Route('/oauth')]
|
||||
class OAuthController extends AbstractController
|
||||
{
|
||||
private OAuthService $oauthService;
|
||||
private Extractor $extractor;
|
||||
|
||||
public function __construct(OAuthService $oauthService, Extractor $extractor)
|
||||
{
|
||||
$this->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'
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
216
hesabixCore/src/Entity/OAuthAccessToken.php
Normal file
216
hesabixCore/src/Entity/OAuthAccessToken.php
Normal file
|
@ -0,0 +1,216 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\OAuthAccessTokenRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: OAuthAccessTokenRepository::class)]
|
||||
class OAuthAccessToken
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255, unique: true)]
|
||||
private ?string $token = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $refreshToken = null;
|
||||
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $scopes = [];
|
||||
|
||||
#[ORM\Column(type: 'datetime')]
|
||||
private ?\DateTimeInterface $expiresAt = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime')]
|
||||
private ?\DateTimeInterface $createdAt = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime', nullable: true)]
|
||||
private ?\DateTimeInterface $lastUsedAt = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?User $user = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?OAuthApplication $application = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
private ?OAuthScope $scope = null;
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $isRevoked = false;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $ipAddress = null;
|
||||
|
||||
#[ORM\Column(length: 500, nullable: true)]
|
||||
private ?string $userAgent = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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();
|
||||
}
|
||||
}
|
332
hesabixCore/src/Entity/OAuthApplication.php
Normal file
332
hesabixCore/src/Entity/OAuthApplication.php
Normal file
|
@ -0,0 +1,332 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\OAuthApplicationRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Serializer\Annotation\SerializedName;
|
||||
|
||||
#[ORM\Entity(repositoryClass: OAuthApplicationRepository::class)]
|
||||
class OAuthApplication
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 3, max: 255)]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column(length: 500, nullable: true)]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Url]
|
||||
private ?string $website = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Url]
|
||||
private ?string $redirectUri = null;
|
||||
|
||||
#[ORM\Column(length: 64, unique: true)]
|
||||
private ?string $clientId = null;
|
||||
|
||||
#[ORM\Column(length: 128)]
|
||||
private ?string $clientSecret = null;
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
#[SerializedName('isActive')]
|
||||
private bool $isActive = true;
|
||||
|
||||
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $logoUrl = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime')]
|
||||
private ?\DateTimeInterface $createdAt = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime')]
|
||||
private ?\DateTimeInterface $updatedAt = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?User $owner = null;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'application', targetEntity: OAuthAccessToken::class, orphanRemoval: true)]
|
||||
private Collection $accessTokens;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'application', targetEntity: OAuthAuthorizationCode::class, orphanRemoval: true)]
|
||||
private Collection $authorizationCodes;
|
||||
|
||||
#[ORM\ManyToMany(targetEntity: OAuthScope::class, inversedBy: 'applications')]
|
||||
private Collection $scopes;
|
||||
|
||||
#[ORM\Column(type: 'json', nullable: true)]
|
||||
private array $allowedScopes = [];
|
||||
|
||||
#[ORM\Column(type: 'integer', options: ['default' => 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<int, OAuthAccessToken>
|
||||
*/
|
||||
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<int, OAuthAuthorizationCode>
|
||||
*/
|
||||
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<int, OAuthScope>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
192
hesabixCore/src/Entity/OAuthAuthorizationCode.php
Normal file
192
hesabixCore/src/Entity/OAuthAuthorizationCode.php
Normal file
|
@ -0,0 +1,192 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\OAuthAuthorizationCodeRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: OAuthAuthorizationCodeRepository::class)]
|
||||
class OAuthAuthorizationCode
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255, unique: true)]
|
||||
private ?string $code = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $redirectUri = null;
|
||||
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $scopes = [];
|
||||
|
||||
#[ORM\Column(type: 'datetime')]
|
||||
private ?\DateTimeInterface $expiresAt = null;
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $isUsed = false;
|
||||
|
||||
#[ORM\Column(type: 'datetime')]
|
||||
private ?\DateTimeInterface $createdAt = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?User $user = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?OAuthApplication $application = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $state = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $codeChallenge = null;
|
||||
|
||||
#[ORM\Column(length: 10, nullable: true)]
|
||||
private ?string $codeChallengeMethod = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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();
|
||||
}
|
||||
}
|
162
hesabixCore/src/Entity/OAuthScope.php
Normal file
162
hesabixCore/src/Entity/OAuthScope.php
Normal file
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\OAuthScopeRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: OAuthScopeRepository::class)]
|
||||
class OAuthScope
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 100, unique: true)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 3, max: 100)]
|
||||
#[Assert\Regex('/^[a-z_]+$/')]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Assert\NotBlank]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $isDefault = false;
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $isSystem = false;
|
||||
|
||||
#[ORM\Column(type: 'datetime')]
|
||||
private ?\DateTimeInterface $createdAt = null;
|
||||
|
||||
#[ORM\ManyToMany(targetEntity: OAuthApplication::class, mappedBy: 'scopes')]
|
||||
private Collection $applications;
|
||||
|
||||
#[ORM\OneToMany(mappedBy: 'scope', targetEntity: OAuthAccessToken::class)]
|
||||
private Collection $accessTokens;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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<int, OAuthApplication>
|
||||
*/
|
||||
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<int, OAuthAccessToken>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
144
hesabixCore/src/Repository/OAuthAccessTokenRepository.php
Normal file
144
hesabixCore/src/Repository/OAuthAccessTokenRepository.php
Normal file
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\OAuthAccessToken;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<OAuthAccessToken>
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
}
|
68
hesabixCore/src/Repository/OAuthApplicationRepository.php
Normal file
68
hesabixCore/src/Repository/OAuthApplicationRepository.php
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\OAuthApplication;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<OAuthApplication>
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\OAuthAuthorizationCode;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<OAuthAuthorizationCode>
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
}
|
75
hesabixCore/src/Repository/OAuthScopeRepository.php
Normal file
75
hesabixCore/src/Repository/OAuthScopeRepository.php
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\OAuthScope;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<OAuthScope>
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
}
|
75
hesabixCore/src/Security/OAuthAuthenticator.php
Normal file
75
hesabixCore/src/Security/OAuthAuthenticator.php
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Service\OAuthService;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
|
||||
class OAuthAuthenticator extends AbstractAuthenticator
|
||||
{
|
||||
private OAuthService $oauthService;
|
||||
|
||||
public function __construct(OAuthService $oauthService)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
303
hesabixCore/src/Service/OAuthService.php
Normal file
303
hesabixCore/src/Service/OAuthService.php
Normal file
|
@ -0,0 +1,303 @@
|
|||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\OAuthApplication;
|
||||
use App\Entity\OAuthAuthorizationCode;
|
||||
use App\Entity\OAuthAccessToken;
|
||||
use App\Entity\OAuthScope;
|
||||
use App\Entity\User;
|
||||
use App\Repository\OAuthApplicationRepository;
|
||||
use App\Repository\OAuthAuthorizationCodeRepository;
|
||||
use App\Repository\OAuthAccessTokenRepository;
|
||||
use App\Repository\OAuthScopeRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
|
||||
class OAuthService
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private OAuthApplicationRepository $applicationRepository;
|
||||
private OAuthAuthorizationCodeRepository $authorizationCodeRepository;
|
||||
private OAuthAccessTokenRepository $accessTokenRepository;
|
||||
private OAuthScopeRepository $scopeRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager,
|
||||
OAuthApplicationRepository $applicationRepository,
|
||||
OAuthAuthorizationCodeRepository $authorizationCodeRepository,
|
||||
OAuthAccessTokenRepository $accessTokenRepository,
|
||||
OAuthScopeRepository $scopeRepository
|
||||
) {
|
||||
$this->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
|
||||
];
|
||||
}
|
||||
}
|
577
webUI/src/components/OAuthManager.vue
Normal file
577
webUI/src/components/OAuthManager.vue
Normal file
|
@ -0,0 +1,577 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- هدر اصلی -->
|
||||
<div class="d-flex align-center mb-8">
|
||||
<div class="d-flex align-center bg-primary-lighten-5 pa-4 rounded-lg">
|
||||
<v-icon size="32" color="primary" class="mr-3">mdi-oauth</v-icon>
|
||||
<div>
|
||||
<h3 class="text-h5 font-weight-medium text-primary mb-1">برنامههای OAuth</h3>
|
||||
<p class="text-caption text-medium-emphasis mb-0">مدیریت برنامههای احراز هویت خارجی</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- توضیحات OAuth -->
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-6"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</template>
|
||||
<div class="text-right">
|
||||
<strong>OAuth چیست؟</strong><br>
|
||||
OAuth یک پروتکل استاندارد برای احراز هویت است که به برنامههای خارجی اجازه میدهد
|
||||
بدون نیاز به رمز عبور، به حساب کاربران دسترسی داشته باشند. این روش امنتر و راحتتر از
|
||||
روشهای سنتی است.
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
<!-- کارت مدیریت برنامهها -->
|
||||
<v-card variant="outlined" class="pa-6" elevation="0">
|
||||
<v-card-title class="text-subtitle-1 font-weight-medium pb-4 d-flex align-center justify-space-between">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon start class="mr-2" color="primary">mdi-oauth</v-icon>
|
||||
مدیریت برنامهها
|
||||
</div>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="showCreateDialog = true"
|
||||
>
|
||||
برنامه جدید
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<v-alert
|
||||
v-if="applications.length === 0"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
هنوز هیچ برنامه OAuth ایجاد نکردهاید. برای شروع، یک برنامه جدید ایجاد کنید.
|
||||
</v-alert>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col
|
||||
v-for="app in applications"
|
||||
:key="app.id"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
>
|
||||
<v-card
|
||||
variant="outlined"
|
||||
class="h-100 oauth-card"
|
||||
:class="{ 'oauth-card-active': app.isActive, 'oauth-card-inactive': !app.isActive }"
|
||||
>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar
|
||||
:color="app.isActive ? 'success' : 'grey'"
|
||||
size="32"
|
||||
class="mr-2"
|
||||
>
|
||||
<v-icon size="16" color="white">
|
||||
{{ app.isActive ? 'mdi-check' : 'mdi-close' }}
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
{{ app.name }}
|
||||
</div>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-dots-vertical"
|
||||
variant="text"
|
||||
v-bind="props"
|
||||
></v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="editApplication(app)" class="oauth-menu-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>ویرایش</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="showStats(app)" class="oauth-menu-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-chart-line</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>آمار</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="regenerateSecret(app)" class="oauth-menu-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>بازسازی کلید</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="revokeTokens(app)" class="oauth-menu-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-logout</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>لغو توکنها</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item @click="deleteApplication(app)" color="error" class="oauth-menu-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="error">mdi-delete</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-error">حذف</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<div v-if="app.description" class="mb-3 oauth-info-text">
|
||||
{{ app.description }}
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey">
|
||||
<div class="mb-1">
|
||||
<strong>Client ID:</strong>
|
||||
<span class="oauth-code">{{ app.clientId }}</span>
|
||||
<v-btn
|
||||
icon="mdi-content-copy"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
@click="copyToClipboard(app.clientId)"
|
||||
></v-btn>
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<strong>Redirect URI:</strong> {{ app.redirectUri }}
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<strong>وضعیت:</strong>
|
||||
<v-chip
|
||||
:color="app.isActive ? 'success' : 'error'"
|
||||
size="x-small"
|
||||
class="ml-1 oauth-status-chip"
|
||||
>
|
||||
{{ app.isActive ? 'فعال' : 'غیرفعال' }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<strong>تایید:</strong>
|
||||
<v-chip
|
||||
:color="app.isVerified ? 'success' : 'warning'"
|
||||
size="x-small"
|
||||
class="ml-1 oauth-status-chip"
|
||||
>
|
||||
{{ app.isVerified ? 'تایید شده' : 'در انتظار تایید' }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Dialog ایجاد/ویرایش برنامه -->
|
||||
<v-dialog v-model="showCreateDialog" max-width="700px" persistent class="oauth-dialog">
|
||||
<v-card>
|
||||
<v-card-title class="text-h6 pa-6 pb-4">
|
||||
<v-icon start class="mr-2" color="primary">mdi-oauth</v-icon>
|
||||
{{ editingApp ? 'ویرایش برنامه OAuth' : 'ایجاد برنامه OAuth جدید' }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-6 pt-0">
|
||||
<v-form ref="form" @submit.prevent="saveApplication">
|
||||
<v-row>
|
||||
<v-col cols="12" class="oauth-form-field">
|
||||
<v-text-field
|
||||
v-model="form.name"
|
||||
label="نام برنامه *"
|
||||
:rules="formRules.name"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-application"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" class="oauth-form-field">
|
||||
<v-textarea
|
||||
v-model="form.description"
|
||||
label="توضیحات"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-text"
|
||||
rows="3"
|
||||
auto-grow
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6" class="oauth-form-field">
|
||||
<v-text-field
|
||||
v-model="form.website"
|
||||
label="آدرس وبسایت"
|
||||
:rules="formRules.website"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-web"
|
||||
type="url"
|
||||
placeholder="https://example.com"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6" class="oauth-form-field">
|
||||
<v-text-field
|
||||
v-model="form.redirectUri"
|
||||
label="آدرس بازگشت (Redirect URI) *"
|
||||
:rules="formRules.redirectUri"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-link"
|
||||
type="url"
|
||||
placeholder="https://example.com/callback"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6" class="oauth-form-field">
|
||||
<v-text-field
|
||||
v-model.number="form.rateLimit"
|
||||
label="محدودیت درخواست (در ساعت)"
|
||||
:rules="formRules.rateLimit"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-speedometer"
|
||||
type="number"
|
||||
min="1"
|
||||
max="10000"
|
||||
hint="تعداد درخواست مجاز در هر ساعت"
|
||||
persistent-hint
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-6">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
@click="cancelEdit"
|
||||
class="mr-2"
|
||||
>
|
||||
انصراف
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="saveApplication"
|
||||
:loading="saving"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
{{ editingApp ? 'ویرایش' : 'ایجاد' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Snackbar برای نمایش پیامها -->
|
||||
<v-snackbar
|
||||
v-model="snackbar.show"
|
||||
:color="snackbar.color"
|
||||
:timeout="snackbar.timeout"
|
||||
location="top"
|
||||
>
|
||||
{{ snackbar.text }}
|
||||
|
||||
<template v-slot:actions>
|
||||
<v-btn
|
||||
color="white"
|
||||
variant="text"
|
||||
@click="snackbar.show = false"
|
||||
>
|
||||
بستن
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
import Swal from 'sweetalert2'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OAuthManager',
|
||||
setup() {
|
||||
const applications = ref([])
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const showCreateDialog = ref(false)
|
||||
const editingApp = ref(null)
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
website: '',
|
||||
redirectUri: '',
|
||||
allowedScopes: [],
|
||||
rateLimit: 1000
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
name: [
|
||||
(v: any) => !!v || 'نام برنامه الزامی است',
|
||||
(v: any) => v.length >= 3 || 'نام برنامه باید حداقل 3 کاراکتر باشد',
|
||||
(v: any) => v.length <= 255 || 'نام برنامه نمیتواند بیشتر از 255 کاراکتر باشد'
|
||||
],
|
||||
redirectUri: [
|
||||
(v: any) => !!v || 'آدرس بازگشت الزامی است',
|
||||
(v: any) => /^https?:\/\/.+/.test(v) || 'آدرس بازگشت باید یک URL معتبر باشد'
|
||||
],
|
||||
website: [
|
||||
(v: any) => !v || /^https?:\/\/.+/.test(v) || 'آدرس وبسایت باید یک URL معتبر باشد'
|
||||
],
|
||||
rateLimit: [
|
||||
(v: any) => v >= 1 || 'محدودیت درخواست باید حداقل 1 باشد',
|
||||
(v: any) => v <= 10000 || 'محدودیت درخواست نمیتواند بیشتر از 10000 باشد'
|
||||
]
|
||||
}
|
||||
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success',
|
||||
timeout: 3000
|
||||
})
|
||||
|
||||
const loadApplications = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get('/api/oauth/applications')
|
||||
applications.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('خطا در بارگذاری برنامهها:', error)
|
||||
showSnackbar('خطا در بارگذاری برنامهها', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const saveApplication = async () => {
|
||||
const { valid } = await form.value.$refs?.validate()
|
||||
if (!valid) {
|
||||
showSnackbar('لطفاً خطاهای فرم را برطرف کنید', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
if (editingApp.value) {
|
||||
await axios.put(`/api/oauth/applications/${editingApp.value.id}`, form.value)
|
||||
showSnackbar('برنامه با موفقیت ویرایش شد', 'success')
|
||||
} else {
|
||||
const response = await axios.post('/api/oauth/applications', form.value)
|
||||
showSnackbar('برنامه با موفقیت ایجاد شد', 'success')
|
||||
|
||||
// نمایش client_id و client_secret
|
||||
if (response.data.data?.client_id) {
|
||||
Swal.fire({
|
||||
title: 'اطلاعات برنامه',
|
||||
html: `
|
||||
<div class="text-right">
|
||||
<p><strong>Client ID:</strong> <code>${response.data.data.client_id}</code></p>
|
||||
<p><strong>Client Secret:</strong> <code>${response.data.data.client_secret}</code></p>
|
||||
<p class="text-warning">⚠️ این اطلاعات را در جای امنی ذخیره کنید!</p>
|
||||
</div>
|
||||
`,
|
||||
icon: 'info'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await loadApplications()
|
||||
cancelEdit()
|
||||
} catch (error) {
|
||||
console.error('خطا در ذخیره برنامه:', error)
|
||||
const errorMessage = error.response?.data?.message || 'خطا در ذخیره برنامه'
|
||||
showSnackbar(errorMessage, 'error')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const editApplication = (app: any) => {
|
||||
editingApp.value = app
|
||||
form.value = {
|
||||
name: app.name,
|
||||
description: app.description || '',
|
||||
website: app.website || '',
|
||||
redirectUri: app.redirectUri,
|
||||
allowedScopes: app.allowedScopes || [],
|
||||
rateLimit: app.rateLimit || 1000
|
||||
}
|
||||
showCreateDialog.value = true
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
editingApp.value = null
|
||||
showCreateDialog.value = false
|
||||
form.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
website: '',
|
||||
redirectUri: '',
|
||||
allowedScopes: [],
|
||||
rateLimit: 1000
|
||||
}
|
||||
}
|
||||
|
||||
const deleteApplication = async (app: any) => {
|
||||
const result = await Swal.fire({
|
||||
title: 'حذف برنامه',
|
||||
text: `آیا از حذف برنامه "${app.name}" اطمینان دارید؟`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'حذف',
|
||||
cancelButtonText: 'انصراف'
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
await axios.delete(`/api/oauth/applications/${app.id}`)
|
||||
showSnackbar('برنامه با موفقیت حذف شد', 'success')
|
||||
await loadApplications()
|
||||
} catch (error) {
|
||||
console.error('خطا در حذف برنامه:', error)
|
||||
const errorMessage = error.response?.data?.message || 'خطا در حذف برنامه'
|
||||
showSnackbar(errorMessage, 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const regenerateSecret = async (app: any) => {
|
||||
const result = await Swal.fire({
|
||||
title: 'بازسازی کلید',
|
||||
text: 'آیا از بازسازی Client Secret اطمینان دارید؟ تمام توکنهای موجود لغو خواهند شد.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'بازسازی',
|
||||
cancelButtonText: 'انصراف'
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await axios.post(`/api/oauth/applications/${app.id}/regenerate-secret`)
|
||||
Swal.fire({
|
||||
title: 'کلید جدید',
|
||||
html: `<p><strong>Client Secret جدید:</strong> <code>${response.data.data.client_secret}</code></p>`,
|
||||
icon: 'success'
|
||||
})
|
||||
await loadApplications()
|
||||
showSnackbar('کلید جدید با موفقیت ایجاد شد', 'success')
|
||||
} catch (error) {
|
||||
console.error('خطا در بازسازی کلید:', error)
|
||||
const errorMessage = error.response?.data?.message || 'خطا در بازسازی کلید'
|
||||
showSnackbar(errorMessage, 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const revokeTokens = async (app: any) => {
|
||||
const result = await Swal.fire({
|
||||
title: 'لغو توکنها',
|
||||
text: 'آیا از لغو تمام توکنهای این برنامه اطمینان دارید؟',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'لغو توکنها',
|
||||
cancelButtonText: 'انصراف'
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await axios.post(`/api/oauth/applications/${app.id}/revoke-tokens`)
|
||||
showSnackbar(`${response.data.data.revokedCount} توکن لغو شد`, 'success')
|
||||
} catch (error) {
|
||||
console.error('خطا در لغو توکنها:', error)
|
||||
const errorMessage = error.response?.data?.message || 'خطا در لغو توکنها'
|
||||
showSnackbar(errorMessage, 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showStats = async (app: any) => {
|
||||
try {
|
||||
const response = await axios.get(`/api/oauth/applications/${app.id}/stats`)
|
||||
const stats = response.data.data
|
||||
|
||||
Swal.fire({
|
||||
title: `آمار استفاده - ${app.name}`,
|
||||
html: `
|
||||
<div class="text-right">
|
||||
<p><strong>کل توکنها:</strong> ${stats.totalTokens}</p>
|
||||
<p><strong>توکنهای فعال:</strong> ${stats.activeTokens}</p>
|
||||
<p><strong>توکنهای منقضی شده:</strong> ${stats.expiredTokens}</p>
|
||||
${stats.lastUsed ? `<p><strong>آخرین استفاده:</strong> ${new Date(stats.lastUsed).toLocaleDateString('fa-IR')}</p>` : ''}
|
||||
</div>
|
||||
`,
|
||||
icon: 'info'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('خطا در بارگذاری آمار:', error)
|
||||
showSnackbar('خطا در بارگذاری آمار', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
showSnackbar('متن در کلیپبورد کپی شد', 'success')
|
||||
} catch (error) {
|
||||
console.error('خطا در کپی:', error)
|
||||
showSnackbar('خطا در کپی کردن متن', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const showSnackbar = (text: string, color = 'success') => {
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
text: text,
|
||||
color: color,
|
||||
timeout: 3000
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadApplications()
|
||||
})
|
||||
|
||||
return {
|
||||
applications,
|
||||
loading,
|
||||
saving,
|
||||
showCreateDialog,
|
||||
editingApp,
|
||||
form,
|
||||
formRules,
|
||||
snackbar,
|
||||
saveApplication,
|
||||
editApplication,
|
||||
cancelEdit,
|
||||
deleteApplication,
|
||||
regenerateSecret,
|
||||
revokeTokens,
|
||||
showStats,
|
||||
copyToClipboard
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import './oauth-styles.css';
|
||||
</style>
|
|
@ -1121,6 +1121,15 @@ const router = createRouter({
|
|||
'title': 'نصب وب اپلیکیشن ',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/oauth/authorize',
|
||||
name: 'oauth_authorize',
|
||||
component: () => import('../views/oauth/authorize.vue'),
|
||||
meta: {
|
||||
'title': 'مجوزدهی OAuth',
|
||||
'login': true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/:catchAll(.*)",
|
||||
name: "not-found",
|
||||
|
|
291
webUI/src/views/oauth/authorize.vue
Normal file
291
webUI/src/views/oauth/authorize.vue
Normal file
|
@ -0,0 +1,291 @@
|
|||
<template>
|
||||
<div class="oauth-authorize-container">
|
||||
<v-container class="fill-height">
|
||||
<v-row justify="center" align="center">
|
||||
<v-col cols="12" sm="8" md="6" lg="4">
|
||||
<v-card class="oauth-card" elevation="8">
|
||||
<v-card-title class="text-center pa-6">
|
||||
<v-icon size="48" color="primary" class="mb-4">mdi-shield-check</v-icon>
|
||||
<h2 class="text-h4 font-weight-bold">مجوزدهی OAuth</h2>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-6">
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="text-center">
|
||||
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
|
||||
<p class="mt-4 text-body-1">در حال بارگذاری...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="text-center">
|
||||
<v-icon size="64" color="error" class="mb-4">mdi-alert-circle</v-icon>
|
||||
<h3 class="text-h6 text-error mb-2">خطا در مجوزدهی</h3>
|
||||
<p class="text-body-1 mb-4">{{ error }}</p>
|
||||
<v-btn color="primary" @click="goBack">بازگشت</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Authorization Form -->
|
||||
<div v-else-if="application" class="authorization-form">
|
||||
<!-- Application Info -->
|
||||
<div class="application-info mb-6">
|
||||
<div class="d-flex align-center mb-4">
|
||||
<v-avatar size="64" color="primary" class="mr-4">
|
||||
<v-icon size="32" color="white">{{ getApplicationIcon() }}</v-icon>
|
||||
</v-avatar>
|
||||
<div>
|
||||
<h3 class="text-h5 font-weight-bold">{{ application.name }}</h3>
|
||||
<p class="text-body-2 text-medium-emphasis">{{ application.description }}</p>
|
||||
<v-chip size="small" color="info" variant="outlined" class="mt-1">
|
||||
{{ application.website }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scopes -->
|
||||
<div class="scopes-section mb-6">
|
||||
<h4 class="text-h6 font-weight-bold mb-3">این برنامه درخواست دسترسی به:</h4>
|
||||
<v-list class="scopes-list">
|
||||
<v-list-item v-for="scope in scopes" :key="scope" class="scope-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="success" size="20">mdi-check-circle</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-body-1">
|
||||
{{ getScopeDescription(scope) }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="scopes.length === 0" class="scope-item">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="info" size="20">mdi-information</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-body-1">
|
||||
هیچ مجوز خاصی درخواست نشده است
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
|
||||
<!-- Security Notice -->
|
||||
<v-alert type="info" variant="tonal" class="mb-6">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-shield-lock</v-icon>
|
||||
</template>
|
||||
<div>
|
||||
<strong>امنیت:</strong> این برنامه فقط به اطلاعات مشخص شده دسترسی خواهد داشت و نمیتواند رمز عبور شما را ببیند.
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons d-flex gap-3">
|
||||
<v-btn
|
||||
block
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="large"
|
||||
@click="denyAccess"
|
||||
:loading="processing"
|
||||
>
|
||||
<v-icon start>mdi-close</v-icon>
|
||||
رد کردن
|
||||
</v-btn>
|
||||
<v-btn
|
||||
block
|
||||
color="primary"
|
||||
size="large"
|
||||
@click="approveAccess"
|
||||
:loading="processing"
|
||||
>
|
||||
<v-icon start>mdi-check</v-icon>
|
||||
تایید و ادامه
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
name: 'OAuthAuthorize',
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
processing: false,
|
||||
error: null,
|
||||
application: null,
|
||||
scopes: [],
|
||||
state: null,
|
||||
clientId: null,
|
||||
redirectUri: null,
|
||||
scope: null
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.initializeAuthorization();
|
||||
},
|
||||
methods: {
|
||||
async initializeAuthorization() {
|
||||
try {
|
||||
// دریافت پارامترها از URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.clientId = urlParams.get('client_id');
|
||||
this.redirectUri = urlParams.get('redirect_uri');
|
||||
this.scope = urlParams.get('scope');
|
||||
this.state = urlParams.get('state');
|
||||
|
||||
// بررسی پارامترهای اجباری
|
||||
if (!this.clientId || !this.redirectUri) {
|
||||
throw new Error('پارامترهای اجباری موجود نیست');
|
||||
}
|
||||
|
||||
// بررسی وضعیت لاگین کاربر
|
||||
const loginCheck = await axios.post('/api/user/check/login');
|
||||
if (!loginCheck.data.Success) {
|
||||
// اگر کاربر لاگین نیست، به صفحه لاگین هدایت شود
|
||||
this.redirectToLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
// دریافت اطلاعات برنامه OAuth
|
||||
await this.loadApplicationInfo();
|
||||
|
||||
// پردازش scope ها
|
||||
this.scopes = this.scope ? this.scope.split(' ') : [];
|
||||
|
||||
this.loading = false;
|
||||
} catch (error) {
|
||||
console.error('Error initializing authorization:', error);
|
||||
this.error = error.response?.data?.message || error.message || 'خطا در بارگذاری اطلاعات';
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadApplicationInfo() {
|
||||
try {
|
||||
const response = await axios.get(`/api/admin/oauth/applications/client/${this.clientId}`);
|
||||
if (response.data.Success) {
|
||||
this.application = response.data.data;
|
||||
} else {
|
||||
throw new Error('برنامه یافت نشد');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('خطا در دریافت اطلاعات برنامه');
|
||||
}
|
||||
},
|
||||
|
||||
redirectToLogin() {
|
||||
const loginUrl = `/user/login?redirect=${encodeURIComponent(window.location.href)}`;
|
||||
this.$router.push(loginUrl);
|
||||
},
|
||||
|
||||
async approveAccess() {
|
||||
await this.processAuthorization(true);
|
||||
},
|
||||
|
||||
async denyAccess() {
|
||||
await this.processAuthorization(false);
|
||||
},
|
||||
|
||||
async processAuthorization(approved) {
|
||||
this.processing = true;
|
||||
try {
|
||||
const response = await axios.post('/api/oauth/authorize', {
|
||||
client_id: this.clientId,
|
||||
redirect_uri: this.redirectUri,
|
||||
scope: this.scope,
|
||||
state: this.state,
|
||||
approved: approved
|
||||
});
|
||||
|
||||
if (response.data.Success) {
|
||||
// هدایت به redirect_uri
|
||||
const redirectUrl = response.data.redirect_url;
|
||||
window.location.href = redirectUrl;
|
||||
} else {
|
||||
throw new Error(response.data.message || 'خطا در پردازش مجوز');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Authorization error:', error);
|
||||
this.error = error.response?.data?.message || error.message || 'خطا در پردازش مجوز';
|
||||
this.processing = false;
|
||||
}
|
||||
},
|
||||
|
||||
getApplicationIcon() {
|
||||
// آیکون پیشفرض برای برنامهها
|
||||
return 'mdi-application';
|
||||
},
|
||||
|
||||
getScopeDescription(scope) {
|
||||
const scopeDescriptions = {
|
||||
'read_profile': 'خواندن اطلاعات پروفایل',
|
||||
'write_profile': 'تغییر اطلاعات پروفایل',
|
||||
'read_business': 'خواندن اطلاعات کسب و کار',
|
||||
'write_business': 'تغییر اطلاعات کسب و کار',
|
||||
'read_financial': 'خواندن اطلاعات مالی',
|
||||
'write_financial': 'تغییر اطلاعات مالی',
|
||||
'read_contacts': 'خواندن لیست مخاطبین',
|
||||
'write_contacts': 'تغییر لیست مخاطبین',
|
||||
'read_documents': 'خواندن اسناد',
|
||||
'write_documents': 'تغییر اسناد',
|
||||
'admin_access': 'دسترسی مدیریتی'
|
||||
};
|
||||
|
||||
return scopeDescriptions[scope] || scope;
|
||||
},
|
||||
|
||||
goBack() {
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oauth-authorize-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.oauth-card {
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.scopes-list {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.scope-item {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.application-info {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 600px) {
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.oauth-card {
|
||||
margin: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
180
webUI/src/views/user/manager/settings/oauth-styles.css
Normal file
180
webUI/src/views/user/manager/settings/oauth-styles.css
Normal file
|
@ -0,0 +1,180 @@
|
|||
/* استایلهای OAuth */
|
||||
|
||||
.oauth-card {
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.oauth-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.oauth-card-active {
|
||||
border-color: var(--v-success-base);
|
||||
background: linear-gradient(135deg, rgba(76, 175, 80, 0.05), rgba(76, 175, 80, 0.02));
|
||||
}
|
||||
|
||||
.oauth-card-inactive {
|
||||
border-color: var(--v-grey-base);
|
||||
background: linear-gradient(135deg, rgba(158, 158, 158, 0.05), rgba(158, 158, 158, 0.02));
|
||||
}
|
||||
|
||||
.oauth-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--v-primary-base), var(--v-secondary-base));
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.oauth-card-active::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.oauth-dialog .v-card-title {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.oauth-dialog .v-card-title::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1), transparent);
|
||||
}
|
||||
|
||||
.oauth-form-field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.oauth-form-field .v-text-field,
|
||||
.oauth-form-field .v-textarea {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.oauth-info-text {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.oauth-code {
|
||||
background: #f5f5f5;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
color: var(--v-primary-base);
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.oauth-status-chip {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.oauth-status-chip:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.oauth-menu-item {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.oauth-menu-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.oauth-stats-card {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.oauth-stats-number {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--v-primary-base);
|
||||
}
|
||||
|
||||
.oauth-stats-label {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* انیمیشنهای OAuth */
|
||||
.oauth-fade-enter-active,
|
||||
.oauth-fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.oauth-fade-enter-from,
|
||||
.oauth-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.oauth-slide-enter-active,
|
||||
.oauth-slide-leave-active {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.oauth-slide-enter-from {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.oauth-slide-leave-to {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* استایلهای مخصوص موبایل */
|
||||
@media (max-width: 768px) {
|
||||
.oauth-dialog {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.oauth-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.oauth-form-field {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* استایلهای مخصوص حالت تاریک */
|
||||
.v-theme--dark .oauth-card-active {
|
||||
background: linear-gradient(135deg, rgba(76, 175, 80, 0.1), rgba(76, 175, 80, 0.05));
|
||||
}
|
||||
|
||||
.v-theme--dark .oauth-card-inactive {
|
||||
background: linear-gradient(135deg, rgba(158, 158, 158, 0.1), rgba(158, 158, 158, 0.05));
|
||||
}
|
||||
|
||||
.v-theme--dark .oauth-code {
|
||||
background: #2d2d2d;
|
||||
border-color: #404040;
|
||||
color: var(--v-primary-base);
|
||||
}
|
||||
|
||||
.v-theme--dark .oauth-stats-card {
|
||||
background: linear-gradient(135deg, #2d2d2d 0%, #404040 100%);
|
||||
border-color: #505050;
|
||||
}
|
||||
|
||||
.v-theme--dark .oauth-info-text {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
528
webUI/src/views/user/manager/settings/oauth.vue
Normal file
528
webUI/src/views/user/manager/settings/oauth.vue
Normal file
|
@ -0,0 +1,528 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<div>
|
||||
<v-icon class="mr-2">mdi-oauth</v-icon>
|
||||
مدیریت برنامههای OAuth
|
||||
</div>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="showCreateDialog = true"
|
||||
:loading="loading"
|
||||
>
|
||||
برنامه جدید
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-alert
|
||||
v-if="applications.length === 0"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
هنوز هیچ برنامه OAuth ایجاد نکردهاید. برای شروع، یک برنامه جدید ایجاد کنید.
|
||||
</v-alert>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col
|
||||
v-for="app in applications"
|
||||
:key="app.id"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
>
|
||||
<v-card variant="outlined" class="h-100">
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar
|
||||
:color="app.isActive ? 'success' : 'grey'"
|
||||
size="32"
|
||||
class="mr-2"
|
||||
>
|
||||
<v-icon size="16" color="white">
|
||||
{{ app.isActive ? 'mdi-check' : 'mdi-close' }}
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
{{ app.name }}
|
||||
</div>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-dots-vertical"
|
||||
variant="text"
|
||||
v-bind="props"
|
||||
></v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="editApplication(app)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>ویرایش</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="showStats(app)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-chart-line</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>آمار</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="regenerateSecret(app)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>بازسازی کلید</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="revokeTokens(app)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-logout</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>لغو توکنها</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item @click="deleteApplication(app)" color="error">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="error">mdi-delete</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-error">حذف</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<div v-if="app.description" class="mb-3">
|
||||
{{ app.description }}
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-grey">
|
||||
<div class="mb-1">
|
||||
<strong>Client ID:</strong>
|
||||
<code class="text-primary">{{ app.clientId }}</code>
|
||||
<v-btn
|
||||
icon="mdi-content-copy"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
@click="copyToClipboard(app.clientId)"
|
||||
></v-btn>
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<strong>Redirect URI:</strong> {{ app.redirectUri }}
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<strong>وضعیت:</strong>
|
||||
<v-chip
|
||||
:color="app.isActive ? 'success' : 'error'"
|
||||
size="x-small"
|
||||
class="ml-1"
|
||||
>
|
||||
{{ app.isActive ? 'فعال' : 'غیرفعال' }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<strong>تایید:</strong>
|
||||
<v-chip
|
||||
:color="app.isVerified ? 'success' : 'warning'"
|
||||
size="x-small"
|
||||
class="ml-1"
|
||||
>
|
||||
{{ app.isVerified ? 'تایید شده' : 'در انتظار تایید' }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Dialog ایجاد/ویرایش برنامه -->
|
||||
<v-dialog v-model="showCreateDialog" max-width="600px" persistent>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ editingApp ? 'ویرایش برنامه' : 'ایجاد برنامه جدید' }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-form ref="form" @submit.prevent="saveApplication">
|
||||
<v-text-field
|
||||
v-model="form.name"
|
||||
label="نام برنامه"
|
||||
:rules="[v => !!v || 'نام برنامه الزامی است']"
|
||||
required
|
||||
></v-text-field>
|
||||
|
||||
<v-textarea
|
||||
v-model="form.description"
|
||||
label="توضیحات"
|
||||
rows="3"
|
||||
></v-textarea>
|
||||
|
||||
<v-text-field
|
||||
v-model="form.website"
|
||||
label="آدرس وبسایت"
|
||||
type="url"
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="form.redirectUri"
|
||||
label="آدرس بازگشت (Redirect URI)"
|
||||
:rules="[v => !!v || 'آدرس بازگشت الزامی است']"
|
||||
required
|
||||
></v-text-field>
|
||||
|
||||
<v-select
|
||||
v-model="form.allowedScopes"
|
||||
:items="availableScopes"
|
||||
item-title="description"
|
||||
item-value="name"
|
||||
label="محدودههای دسترسی"
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
></v-select>
|
||||
|
||||
<v-text-field
|
||||
v-model.number="form.rateLimit"
|
||||
label="محدودیت درخواست (در ساعت)"
|
||||
type="number"
|
||||
min="1"
|
||||
max="10000"
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="cancelEdit">انصراف</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="saveApplication"
|
||||
:loading="saving"
|
||||
>
|
||||
{{ editingApp ? 'ویرایش' : 'ایجاد' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Dialog آمار -->
|
||||
<v-dialog v-model="showStatsDialog" max-width="500px">
|
||||
<v-card>
|
||||
<v-card-title>آمار استفاده - {{ selectedApp?.name }}</v-card-title>
|
||||
|
||||
<v-card-text v-if="appStats">
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-key</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>کل توکنها</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<v-chip>{{ appStats.totalTokens }}</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="success">mdi-check-circle</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>توکنهای فعال</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<v-chip color="success">{{ appStats.activeTokens }}</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="warning">mdi-clock</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>توکنهای منقضی شده</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<v-chip color="warning">{{ appStats.expiredTokens }}</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="appStats.lastUsed">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-calendar</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>آخرین استفاده</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<span class="text-caption">{{ formatDate(appStats.lastUsed) }}</span>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="showStatsDialog = false">بستن</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
import Swal from 'sweetalert2'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OAuthManagement',
|
||||
setup() {
|
||||
const applications = ref([])
|
||||
const availableScopes = ref([])
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const showCreateDialog = ref(false)
|
||||
const showStatsDialog = ref(false)
|
||||
const editingApp = ref(null)
|
||||
const selectedApp = ref(null)
|
||||
const appStats = ref(null)
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
website: '',
|
||||
redirectUri: '',
|
||||
allowedScopes: [],
|
||||
rateLimit: 1000
|
||||
})
|
||||
|
||||
const loadApplications = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get('/api/oauth/applications')
|
||||
applications.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('خطا در بارگذاری برنامهها:', error)
|
||||
Swal.fire('خطا', 'خطا در بارگذاری برنامهها', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadScopes = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/oauth/scopes')
|
||||
availableScopes.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('خطا در بارگذاری محدودهها:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const saveApplication = async () => {
|
||||
const { valid } = await form.value.$refs?.validate()
|
||||
if (!valid) return
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
if (editingApp.value) {
|
||||
await axios.put(`/api/oauth/applications/${editingApp.value.id}`, form.value)
|
||||
Swal.fire('موفق', 'برنامه با موفقیت ویرایش شد', 'success')
|
||||
} else {
|
||||
const response = await axios.post('/api/oauth/applications', form.value)
|
||||
Swal.fire('موفق', 'برنامه با موفقیت ایجاد شد', 'success')
|
||||
|
||||
// نمایش client_id و client_secret
|
||||
if (response.data.data?.client_id) {
|
||||
Swal.fire({
|
||||
title: 'اطلاعات برنامه',
|
||||
html: `
|
||||
<div class="text-right">
|
||||
<p><strong>Client ID:</strong> <code>${response.data.data.client_id}</code></p>
|
||||
<p><strong>Client Secret:</strong> <code>${response.data.data.client_secret}</code></p>
|
||||
<p class="text-warning">⚠️ این اطلاعات را در جای امنی ذخیره کنید!</p>
|
||||
</div>
|
||||
`,
|
||||
icon: 'info'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await loadApplications()
|
||||
cancelEdit()
|
||||
} catch (error) {
|
||||
console.error('خطا در ذخیره برنامه:', error)
|
||||
Swal.fire('خطا', 'خطا در ذخیره برنامه', 'error')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const editApplication = (app) => {
|
||||
editingApp.value = app
|
||||
form.value = {
|
||||
name: app.name,
|
||||
description: app.description || '',
|
||||
website: app.website || '',
|
||||
redirectUri: app.redirectUri,
|
||||
allowedScopes: app.allowedScopes || [],
|
||||
rateLimit: app.rateLimit || 1000
|
||||
}
|
||||
showCreateDialog.value = true
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
editingApp.value = null
|
||||
showCreateDialog.value = false
|
||||
form.value = {
|
||||
name: '',
|
||||
description: '',
|
||||
website: '',
|
||||
redirectUri: '',
|
||||
allowedScopes: [],
|
||||
rateLimit: 1000
|
||||
}
|
||||
}
|
||||
|
||||
const deleteApplication = async (app) => {
|
||||
const result = await Swal.fire({
|
||||
title: 'حذف برنامه',
|
||||
text: `آیا از حذف برنامه "${app.name}" اطمینان دارید؟`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'حذف',
|
||||
cancelButtonText: 'انصراف'
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
await axios.delete(`/api/oauth/applications/${app.id}`)
|
||||
Swal.fire('موفق', 'برنامه با موفقیت حذف شد', 'success')
|
||||
await loadApplications()
|
||||
} catch (error) {
|
||||
console.error('خطا در حذف برنامه:', error)
|
||||
Swal.fire('خطا', 'خطا در حذف برنامه', 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const regenerateSecret = async (app) => {
|
||||
const result = await Swal.fire({
|
||||
title: 'بازسازی کلید',
|
||||
text: 'آیا از بازسازی Client Secret اطمینان دارید؟ تمام توکنهای موجود لغو خواهند شد.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'بازسازی',
|
||||
cancelButtonText: 'انصراف'
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await axios.post(`/api/oauth/applications/${app.id}/regenerate-secret`)
|
||||
Swal.fire({
|
||||
title: 'کلید جدید',
|
||||
html: `<p><strong>Client Secret جدید:</strong> <code>${response.data.data.client_secret}</code></p>`,
|
||||
icon: 'success'
|
||||
})
|
||||
await loadApplications()
|
||||
} catch (error) {
|
||||
console.error('خطا در بازسازی کلید:', error)
|
||||
Swal.fire('خطا', 'خطا در بازسازی کلید', 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const revokeTokens = async (app) => {
|
||||
const result = await Swal.fire({
|
||||
title: 'لغو توکنها',
|
||||
text: 'آیا از لغو تمام توکنهای این برنامه اطمینان دارید؟',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'لغو توکنها',
|
||||
cancelButtonText: 'انصراف'
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await axios.post(`/api/oauth/applications/${app.id}/revoke-tokens`)
|
||||
Swal.fire('موفق', `${response.data.data.revokedCount} توکن لغو شد`, 'success')
|
||||
} catch (error) {
|
||||
console.error('خطا در لغو توکنها:', error)
|
||||
Swal.fire('خطا', 'خطا در لغو توکنها', 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showStats = async (app) => {
|
||||
selectedApp.value = app
|
||||
try {
|
||||
const response = await axios.get(`/api/oauth/applications/${app.id}/stats`)
|
||||
appStats.value = response.data.data
|
||||
showStatsDialog.value = true
|
||||
} catch (error) {
|
||||
console.error('خطا در بارگذاری آمار:', error)
|
||||
Swal.fire('خطا', 'خطا در بارگذاری آمار', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
Swal.fire({
|
||||
title: 'کپی شد',
|
||||
text: 'متن در کلیپبورد کپی شد',
|
||||
icon: 'success',
|
||||
timer: 1500,
|
||||
showConfirmButton: false
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('خطا در کپی:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString('fa-IR')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadApplications()
|
||||
loadScopes()
|
||||
})
|
||||
|
||||
return {
|
||||
applications,
|
||||
availableScopes,
|
||||
loading,
|
||||
saving,
|
||||
showCreateDialog,
|
||||
showStatsDialog,
|
||||
editingApp,
|
||||
selectedApp,
|
||||
appStats,
|
||||
form,
|
||||
saveApplication,
|
||||
editApplication,
|
||||
cancelEdit,
|
||||
deleteApplication,
|
||||
regenerateSecret,
|
||||
revokeTokens,
|
||||
showStats,
|
||||
copyToClipboard,
|
||||
formatDate
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
background: #f5f5f5;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
|
@ -12,7 +12,8 @@ export default defineComponent({
|
|||
{ title: 'تنظیمات پایه', icon: 'mdi-cog' },
|
||||
{ title: 'درگاههای پرداخت', icon: 'mdi-credit-card' },
|
||||
{ title: 'پنل استعلامات', icon: 'mdi-magnify' },
|
||||
{ title: 'جادوگر هوش مصنوعی', icon: 'mdi-robot' }
|
||||
{ title: 'جادوگر هوش مصنوعی', icon: 'mdi-robot' },
|
||||
{ title: 'برنامههای OAuth', icon: 'mdi-oauth' }
|
||||
],
|
||||
gatepays: [
|
||||
{
|
||||
|
@ -74,6 +75,57 @@ export default defineComponent({
|
|||
outputTokenPrice: 0,
|
||||
aiPrompt: '',
|
||||
aiDebugMode: false,
|
||||
// متغیرهای OAuth
|
||||
oauthApplications: [],
|
||||
showOAuthDialog: false,
|
||||
editingOAuthApp: null,
|
||||
oauthForm: {
|
||||
name: '',
|
||||
description: '',
|
||||
website: '',
|
||||
redirectUri: '',
|
||||
allowedScopes: [],
|
||||
rateLimit: 1000,
|
||||
ipWhitelist: []
|
||||
},
|
||||
newIpAddress: '',
|
||||
availableScopes: [
|
||||
{ name: 'read_profile', description: 'دسترسی به اطلاعات پروفایل کاربر' },
|
||||
{ name: 'write_profile', description: 'تغییر اطلاعات پروفایل کاربر' },
|
||||
{ name: 'read_business', description: 'دسترسی به اطلاعات کسب و کار' },
|
||||
{ name: 'write_business', description: 'تغییر اطلاعات کسب و کار' },
|
||||
{ name: 'read_financial', description: 'دسترسی به اطلاعات مالی' },
|
||||
{ name: 'write_financial', description: 'تغییر اطلاعات مالی' },
|
||||
{ name: 'read_contacts', description: 'دسترسی به لیست مخاطبین' },
|
||||
{ name: 'write_contacts', description: 'تغییر لیست مخاطبین' },
|
||||
{ name: 'read_documents', description: 'دسترسی به اسناد' },
|
||||
{ name: 'write_documents', description: 'تغییر اسناد' },
|
||||
{ name: 'admin_access', description: 'دسترسی مدیریتی (فقط برای برنامههای معتبر)' }
|
||||
],
|
||||
oauthFormRules: {
|
||||
name: [
|
||||
v => !!v || 'نام برنامه الزامی است',
|
||||
v => v.length >= 3 || 'نام برنامه باید حداقل 3 کاراکتر باشد',
|
||||
v => v.length <= 255 || 'نام برنامه نمیتواند بیشتر از 255 کاراکتر باشد'
|
||||
],
|
||||
redirectUri: [
|
||||
v => !!v || 'آدرس بازگشت الزامی است',
|
||||
v => /^https?:\/\/.+/.test(v) || 'آدرس بازگشت باید یک URL معتبر باشد'
|
||||
],
|
||||
website: [
|
||||
v => !v || /^https?:\/\/.+/.test(v) || 'آدرس وبسایت باید یک URL معتبر باشد'
|
||||
],
|
||||
rateLimit: [
|
||||
v => v >= 1 || 'محدودیت درخواست باید حداقل 1 باشد',
|
||||
v => v <= 10000 || 'محدودیت درخواست نمیتواند بیشتر از 10000 باشد'
|
||||
]
|
||||
},
|
||||
oauthSnackbar: {
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success',
|
||||
timeout: 3000
|
||||
},
|
||||
aiAgentSources: [
|
||||
{ title: 'GapGPT', value: 'gapgpt', subtitle: 'gapgpt.app' },
|
||||
{ title: 'AvalAI', value: 'avalai', subtitle: 'avalai.ir' },
|
||||
|
@ -248,10 +300,315 @@ export default defineComponent({
|
|||
});
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
// متدهای OAuth
|
||||
async loadOAuthApplications() {
|
||||
try {
|
||||
const response = await axios.get('/api/admin/oauth/applications')
|
||||
// تبدیل فیلدها از snake_case به camelCase
|
||||
this.oauthApplications = (response.data.data || []).map(app => ({
|
||||
...app,
|
||||
isActive: app.isActive || app.active || false, // پشتیبانی از هر دو فیلد
|
||||
clientId: app.clientId,
|
||||
redirectUri: app.redirectUri,
|
||||
allowedScopes: app.allowedScopes || [],
|
||||
rateLimit: app.rateLimit || 1000
|
||||
}))
|
||||
|
||||
} catch (error) {
|
||||
console.error('خطا در بارگذاری برنامههای OAuth:', error)
|
||||
}
|
||||
},
|
||||
|
||||
async saveOAuthApplication() {
|
||||
// اعتبارسنجی فرم
|
||||
const { valid } = await this.$refs.oauthForm?.validate()
|
||||
if (!valid) {
|
||||
this.showOAuthSnackbar('لطفاً خطاهای فرم را برطرف کنید', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.editingOAuthApp) {
|
||||
await axios.put(`/api/admin/oauth/applications/${this.editingOAuthApp.id}`, this.oauthForm)
|
||||
this.showOAuthSnackbar('برنامه با موفقیت ویرایش شد', 'success')
|
||||
} else {
|
||||
const response = await axios.post('/api/admin/oauth/applications', this.oauthForm)
|
||||
this.showOAuthSnackbar('برنامه با موفقیت ایجاد شد', 'success')
|
||||
|
||||
// نمایش client_id و client_secret
|
||||
if (response.data.data?.client_id) {
|
||||
Swal.fire({
|
||||
title: 'اطلاعات برنامه',
|
||||
html: `
|
||||
<div class="text-right">
|
||||
<p><strong>Client ID:</strong> <code>${response.data.data.client_id}</code></p>
|
||||
<p><strong>Client Secret:</strong> <code>${response.data.data.client_secret}</code></p>
|
||||
<p class="text-warning">⚠️ این اطلاعات را در جای امنی ذخیره کنید!</p>
|
||||
</div>
|
||||
`,
|
||||
icon: 'info'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await this.loadOAuthApplications()
|
||||
this.cancelOAuthEdit()
|
||||
} catch (error) {
|
||||
console.error('خطا در ذخیره برنامه OAuth:', error)
|
||||
const errorMessage = error.response?.data?.message || error.response?.data?.data || 'خطا در ذخیره برنامه'
|
||||
this.showOAuthSnackbar(errorMessage, 'error')
|
||||
}
|
||||
},
|
||||
|
||||
editOAuthApp(app) {
|
||||
this.editingOAuthApp = app
|
||||
this.oauthForm = {
|
||||
name: app.name,
|
||||
description: app.description || '',
|
||||
website: app.website || '',
|
||||
redirectUri: app.redirectUri,
|
||||
allowedScopes: app.allowedScopes || [],
|
||||
rateLimit: app.rateLimit || 1000,
|
||||
ipWhitelist: app.ipWhitelist || []
|
||||
}
|
||||
this.showOAuthDialog = true
|
||||
},
|
||||
|
||||
// IP Management Methods
|
||||
addIpAddress() {
|
||||
if (this.newIpAddress && this.isValidIp(this.newIpAddress)) {
|
||||
if (!this.oauthForm.ipWhitelist.includes(this.newIpAddress)) {
|
||||
this.oauthForm.ipWhitelist.push(this.newIpAddress)
|
||||
this.newIpAddress = ''
|
||||
this.showOAuthSnackbar('آدرس IP اضافه شد', 'success')
|
||||
} else {
|
||||
this.showOAuthSnackbar('این آدرس IP قبلاً اضافه شده است', 'warning')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
removeIpAddress(index) {
|
||||
this.oauthForm.ipWhitelist.splice(index, 1)
|
||||
this.showOAuthSnackbar('آدرس IP حذف شد', 'success')
|
||||
},
|
||||
|
||||
isValidIp(ip) {
|
||||
// IP validation regex (supports both single IP and CIDR notation)
|
||||
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\/(?:3[0-2]|[12]?[0-9]))?$/
|
||||
return ipRegex.test(ip)
|
||||
},
|
||||
|
||||
ipValidationRule(value) {
|
||||
if (!value) return true
|
||||
return this.isValidIp(value) || 'آدرس IP نامعتبر است'
|
||||
},
|
||||
|
||||
cancelOAuthEdit() {
|
||||
this.editingOAuthApp = null
|
||||
this.showOAuthDialog = false
|
||||
this.newIpAddress = ''
|
||||
this.oauthForm = {
|
||||
name: '',
|
||||
description: '',
|
||||
website: '',
|
||||
redirectUri: '',
|
||||
allowedScopes: [],
|
||||
rateLimit: 1000,
|
||||
ipWhitelist: []
|
||||
}
|
||||
},
|
||||
|
||||
async deleteOAuthApp(app) {
|
||||
const result = await Swal.fire({
|
||||
title: 'حذف برنامه',
|
||||
text: `آیا از حذف برنامه "${app.name}" اطمینان دارید؟`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'حذف',
|
||||
cancelButtonText: 'انصراف'
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
await axios.delete(`/api/admin/oauth/applications/${app.id}`)
|
||||
this.showOAuthSnackbar('برنامه با موفقیت حذف شد', 'success')
|
||||
await this.loadOAuthApplications()
|
||||
} catch (error) {
|
||||
console.error('خطا در حذف برنامه OAuth:', error)
|
||||
const errorMessage = error.response?.data?.message || error.response?.data?.data || 'خطا در حذف برنامه'
|
||||
this.showOAuthSnackbar(errorMessage, 'error')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async regenerateOAuthSecret(app) {
|
||||
const result = await Swal.fire({
|
||||
title: 'بازسازی کلید',
|
||||
text: 'آیا از بازسازی Client Secret اطمینان دارید؟ تمام توکنهای موجود لغو خواهند شد.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'بازسازی',
|
||||
cancelButtonText: 'انصراف'
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await axios.post(`/api/admin/oauth/applications/${app.id}/regenerate-secret`)
|
||||
Swal.fire({
|
||||
title: 'کلید جدید',
|
||||
html: `<p><strong>Client Secret جدید:</strong> <code>${response.data.data.client_secret}</code></p>`,
|
||||
icon: 'success'
|
||||
})
|
||||
await this.loadOAuthApplications()
|
||||
this.showOAuthSnackbar('کلید جدید با موفقیت ایجاد شد', 'success')
|
||||
} catch (error) {
|
||||
console.error('خطا در بازسازی کلید OAuth:', error)
|
||||
const errorMessage = error.response?.data?.message || error.response?.data?.data || 'خطا در بازسازی کلید'
|
||||
this.showOAuthSnackbar(errorMessage, 'error')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async toggleOAuthStatus(app) {
|
||||
const action = app.isActive ? 'غیرفعال کردن' : 'فعال کردن'
|
||||
const result = await Swal.fire({
|
||||
title: `${action} برنامه`,
|
||||
text: `آیا از ${action} برنامه "${app.name}" اطمینان دارید؟`,
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: action,
|
||||
cancelButtonText: 'انصراف'
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await axios.post(`/api/admin/oauth/applications/${app.id}/toggle-status`)
|
||||
|
||||
// بهروزرسانی کامل آرایه برای اطمینان از reactive بودن
|
||||
await this.loadOAuthApplications()
|
||||
// اطمینان از بهروزرسانی UI با تاخیر
|
||||
setTimeout(() => {
|
||||
this.$forceUpdate()
|
||||
}, 100)
|
||||
|
||||
this.showOAuthSnackbar(response.data.data.message, 'success')
|
||||
} catch (error) {
|
||||
console.error('خطا در تغییر وضعیت OAuth:', error)
|
||||
const errorMessage = error.response?.data?.message || error.response?.data?.data || 'خطا در تغییر وضعیت'
|
||||
this.showOAuthSnackbar(errorMessage, 'error')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async revokeOAuthTokens(app) {
|
||||
const result = await Swal.fire({
|
||||
title: 'لغو توکنها',
|
||||
text: 'آیا از لغو تمام توکنهای این برنامه اطمینان دارید؟',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'لغو توکنها',
|
||||
cancelButtonText: 'انصراف'
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const response = await axios.post(`/api/admin/oauth/applications/${app.id}/revoke-tokens`)
|
||||
this.showOAuthSnackbar(`${response.data.data.revoked_count || 0} توکن لغو شد`, 'success')
|
||||
} catch (error) {
|
||||
console.error('خطا در لغو توکنهای OAuth:', error)
|
||||
const errorMessage = error.response?.data?.message || error.response?.data?.data || 'خطا در لغو توکنها'
|
||||
this.showOAuthSnackbar(errorMessage, 'error')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async showOAuthStats(app) {
|
||||
try {
|
||||
const response = await axios.get(`/api/admin/oauth/applications/${app.id}/stats`)
|
||||
const stats = response.data.data
|
||||
|
||||
Swal.fire({
|
||||
title: `آمار استفاده - ${app.name}`,
|
||||
html: `
|
||||
<div class="text-right">
|
||||
<p><strong>کل توکنها:</strong> ${stats.total_tokens || 0}</p>
|
||||
<p><strong>توکنهای فعال:</strong> ${stats.active_tokens || 0}</p>
|
||||
<p><strong>توکنهای منقضی شده:</strong> ${stats.expired_tokens || 0}</p>
|
||||
${stats.last_used ? `<p><strong>آخرین استفاده:</strong> ${new Date(stats.last_used).toLocaleDateString('fa-IR')}</p>` : ''}
|
||||
</div>
|
||||
`,
|
||||
icon: 'info'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('خطا در بارگذاری آمار OAuth:', error)
|
||||
Swal.fire('خطا', 'خطا در بارگذاری آمار', 'error')
|
||||
}
|
||||
},
|
||||
|
||||
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)
|
||||
}
|
||||
},
|
||||
|
||||
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')
|
||||
}
|
||||
},
|
||||
|
||||
showOAuthSnackbar(text, color = 'success') {
|
||||
this.oauthSnackbar = {
|
||||
show: true,
|
||||
text: text,
|
||||
color: color,
|
||||
timeout: 3000
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.loadData();
|
||||
this.loadOAuthApplications();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
@ -985,9 +1342,552 @@ export default defineComponent({
|
|||
</v-row>
|
||||
</v-card-text>
|
||||
</v-window-item>
|
||||
|
||||
<!-- تب پنجم: برنامههای OAuth -->
|
||||
<v-window-item :value="4">
|
||||
<v-card-text class="pa-8">
|
||||
<div class="d-flex align-center mb-8">
|
||||
<div class="d-flex align-center bg-primary-lighten-5 pa-4 rounded-lg">
|
||||
<v-icon size="32" color="primary" class="mr-3">mdi-oauth</v-icon>
|
||||
<div>
|
||||
<h3 class="text-h5 font-weight-medium text-primary mb-1">برنامههای OAuth</h3>
|
||||
<p class="text-caption text-medium-emphasis mb-0">مدیریت برنامههای احراز هویت خارجی</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header Section -->
|
||||
<div class="mb-8">
|
||||
<div class="d-flex align-center justify-space-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-h4 font-weight-bold text-primary mb-2">
|
||||
<v-icon start size="32" color="primary">mdi-oauth</v-icon>
|
||||
مدیریت برنامههای OAuth
|
||||
</h2>
|
||||
<p class="text-body-1 text-medium-emphasis">
|
||||
برنامههای خارجی که میتوانند از حساب کاربران شما استفاده کنند
|
||||
</p>
|
||||
</div>
|
||||
<v-btn
|
||||
color="primary"
|
||||
size="large"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="showOAuthDialog = true"
|
||||
elevation="2"
|
||||
class="text-none"
|
||||
>
|
||||
ایجاد برنامه جدید
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<v-row class="mb-6">
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card variant="outlined" class="text-center pa-4" elevation="0">
|
||||
<v-icon size="32" color="primary" class="mb-2">mdi-application</v-icon>
|
||||
<div class="text-h5 font-weight-bold text-primary">{{ oauthApplications.length }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">کل برنامهها</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card variant="outlined" class="text-center pa-4" elevation="0">
|
||||
<v-icon size="32" color="success" class="mb-2">mdi-check-circle</v-icon>
|
||||
<div class="text-h5 font-weight-bold text-success">{{ oauthApplications.filter(app => app.isActive).length }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">فعال</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card variant="outlined" class="text-center pa-4" elevation="0">
|
||||
<v-icon size="32" color="warning" class="mb-2">mdi-pause-circle</v-icon>
|
||||
<div class="text-h5 font-weight-bold text-warning">{{ oauthApplications.filter(app => !app.isActive).length }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">غیرفعال</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card variant="outlined" class="text-center pa-4" elevation="0">
|
||||
<v-icon size="32" color="info" class="mb-2">mdi-clock-outline</v-icon>
|
||||
<div class="text-h5 font-weight-bold text-info">{{ oauthApplications.filter(app => new Date(app.createdAt.timestamp * 1000) > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)).length }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">جدید (7 روز)</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Info Alert -->
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-6"
|
||||
border="start"
|
||||
border-color="primary"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon size="24" color="primary">mdi-information</v-icon>
|
||||
</template>
|
||||
<div class="text-right">
|
||||
<div class="text-subtitle-1 font-weight-medium mb-2">OAuth چیست؟</div>
|
||||
<p class="text-body-2 mb-0">
|
||||
OAuth یک پروتکل استاندارد برای احراز هویت است که به برنامههای خارجی اجازه میدهد
|
||||
بدون نیاز به رمز عبور، به حساب کاربران دسترسی داشته باشند. این روش امنتر و راحتتر از
|
||||
روشهای سنتی است.
|
||||
</p>
|
||||
</div>
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<!-- Applications Section -->
|
||||
<div>
|
||||
<div class="d-flex align-center justify-space-between mb-4">
|
||||
<h3 class="text-h6 font-weight-medium">برنامههای شما</h3>
|
||||
<v-chip
|
||||
:color="oauthApplications.length > 0 ? 'success' : 'warning'"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
>
|
||||
{{ oauthApplications.length }} برنامه
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<v-card
|
||||
v-if="oauthApplications.length === 0"
|
||||
variant="outlined"
|
||||
class="text-center pa-8"
|
||||
elevation="0"
|
||||
>
|
||||
<v-icon size="64" color="grey-lighten-1" class="mb-4">mdi-application-outline</v-icon>
|
||||
<h3 class="text-h6 font-weight-medium text-grey-darken-1 mb-2">
|
||||
هنوز برنامهای ایجاد نکردهاید
|
||||
</h3>
|
||||
<p class="text-body-2 text-grey mb-6">
|
||||
برای شروع استفاده از OAuth، اولین برنامه خود را ایجاد کنید
|
||||
</p>
|
||||
<v-btn
|
||||
color="primary"
|
||||
size="large"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="showOAuthDialog = true"
|
||||
elevation="2"
|
||||
>
|
||||
ایجاد اولین برنامه
|
||||
</v-btn>
|
||||
</v-card>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col
|
||||
v-for="app in oauthApplications"
|
||||
:key="`${app.id}-${app.isActive}`"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
>
|
||||
<v-card
|
||||
variant="outlined"
|
||||
class="h-100 application-card"
|
||||
:class="{ 'inactive-app': !app.isActive }"
|
||||
elevation="0"
|
||||
hover
|
||||
>
|
||||
<!-- Header -->
|
||||
<v-card-title class="d-flex justify-space-between align-center pa-4 pb-2">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar
|
||||
:color="app.isActive ? 'success' : 'grey'"
|
||||
size="40"
|
||||
class="mr-3"
|
||||
>
|
||||
<v-icon size="20" color="white">
|
||||
{{ app.isActive ? 'mdi-check' : 'mdi-pause' }}
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
<div>
|
||||
<div class="text-subtitle-1 font-weight-medium text-truncate" style="max-width: 200px;">
|
||||
{{ app.name }}
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
{{ new Date(app.createdAt.timestamp * 1000).toLocaleDateString('fa-IR') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<v-chip
|
||||
:color="app.isActive ? 'success' : 'error'"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ app.isActive ? 'فعال' : 'غیرفعال' }}
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
|
||||
<!-- Content -->
|
||||
<v-card-text class="pa-4 pt-0">
|
||||
<!-- Description -->
|
||||
<div v-if="app.description" class="mb-4">
|
||||
<p class="text-body-2 text-medium-emphasis mb-0">
|
||||
{{ app.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Info Grid -->
|
||||
<div class="info-grid mb-4">
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<v-icon size="16" class="mr-1">mdi-key</v-icon>
|
||||
Client ID
|
||||
</div>
|
||||
<div class="info-value">
|
||||
<code class="text-primary">{{ app.clientId }}</code>
|
||||
<v-btn
|
||||
icon="mdi-content-copy"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
@click="copyToClipboard(app.clientId)"
|
||||
class="ml-1"
|
||||
></v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<v-icon size="16" class="mr-1">mdi-link</v-icon>
|
||||
Redirect URI
|
||||
</div>
|
||||
<div class="info-value text-truncate">
|
||||
{{ app.redirectUri }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">
|
||||
<v-icon size="16" class="mr-1">mdi-shield</v-icon>
|
||||
محدوده دسترسی
|
||||
</div>
|
||||
<div class="info-value">
|
||||
<v-chip
|
||||
v-for="scope in app.allowedScopes"
|
||||
:key="scope"
|
||||
size="x-small"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
class="mr-1 mb-1"
|
||||
>
|
||||
{{ scope }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
@click="editOAuthApp(app)"
|
||||
prepend-icon="mdi-pencil"
|
||||
>
|
||||
ویرایش
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="info"
|
||||
@click="showOAuthStats(app)"
|
||||
prepend-icon="mdi-chart-line"
|
||||
>
|
||||
آمار
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="outlined"
|
||||
:color="app.isActive ? 'error' : 'success'"
|
||||
@click="toggleOAuthStatus(app)"
|
||||
:prepend-icon="app.isActive ? 'mdi-pause' : 'mdi-play'"
|
||||
>
|
||||
{{ app.isActive ? 'غیرفعال' : 'فعال' }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Menu -->
|
||||
<v-card-actions class="pa-4 pt-0">
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-dots-vertical"
|
||||
variant="text"
|
||||
size="small"
|
||||
v-bind="props"
|
||||
></v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="regenerateOAuthSecret(app)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-refresh</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>بازسازی کلید</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="revokeOAuthTokens(app)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-logout</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>لغو توکنها</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider></v-divider>
|
||||
<v-list-item @click="deleteOAuthApp(app)" color="error">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="error">mdi-delete</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-error">حذف</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card>
|
||||
</v-container>
|
||||
|
||||
<!-- Dialog ایجاد/ویرایش برنامه OAuth -->
|
||||
<v-dialog v-model="showOAuthDialog" max-width="700px" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="text-h6 pa-6 pb-4">
|
||||
<v-icon start class="mr-2" color="primary">mdi-oauth</v-icon>
|
||||
{{ editingOAuthApp ? 'ویرایش برنامه OAuth' : 'ایجاد برنامه OAuth جدید' }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-6 pt-0">
|
||||
<v-form ref="oauthForm" @submit.prevent="saveOAuthApplication">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="oauthForm.name"
|
||||
label="نام برنامه *"
|
||||
:rules="oauthFormRules.name"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-application"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="oauthForm.description"
|
||||
label="توضیحات"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-text"
|
||||
rows="3"
|
||||
auto-grow
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="oauthForm.website"
|
||||
label="آدرس وبسایت"
|
||||
:rules="oauthFormRules.website"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-web"
|
||||
type="url"
|
||||
placeholder="https://example.com"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="oauthForm.redirectUri"
|
||||
label="آدرس بازگشت (Redirect URI) *"
|
||||
:rules="oauthFormRules.redirectUri"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-link"
|
||||
type="url"
|
||||
placeholder="https://example.com/callback"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model.number="oauthForm.rateLimit"
|
||||
label="محدودیت درخواست (در ساعت)"
|
||||
:rules="oauthFormRules.rateLimit"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-speedometer"
|
||||
type="number"
|
||||
min="1"
|
||||
max="10000"
|
||||
hint="تعداد درخواست مجاز در هر ساعت"
|
||||
persistent-hint
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<!-- IP Whitelist -->
|
||||
<v-col cols="12">
|
||||
<v-card variant="outlined" class="pa-4">
|
||||
<v-card-title class="text-subtitle-1 font-weight-medium pb-2">
|
||||
<v-icon start class="mr-2" color="primary">mdi-ip-network</v-icon>
|
||||
آدرسهای IP مجاز
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-0">
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
در صورت خالی بودن، از هر آدرس IP میتوان به برنامه متصل شد.
|
||||
برای محدود کردن دسترسی، آدرسهای IP مجاز را وارد کنید.
|
||||
</p>
|
||||
|
||||
<div class="d-flex align-center mb-3">
|
||||
<v-text-field
|
||||
v-model="newIpAddress"
|
||||
label="آدرس IP"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-ip"
|
||||
placeholder="192.168.1.1 یا 192.168.1.0/24"
|
||||
class="mr-2"
|
||||
:rules="[ipValidationRule]"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="addIpAddress"
|
||||
:disabled="!newIpAddress || !isValidIp(newIpAddress)"
|
||||
prepend-icon="mdi-plus"
|
||||
>
|
||||
افزودن
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-chip-group v-if="oauthForm.ipWhitelist.length > 0">
|
||||
<v-chip
|
||||
v-for="(ip, index) in oauthForm.ipWhitelist"
|
||||
:key="index"
|
||||
closable
|
||||
@click:close="removeIpAddress(index)"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ ip }}
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
|
||||
<v-alert
|
||||
v-else
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mt-2"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</template>
|
||||
هیچ آدرس IP محدودیتی تعریف نشده است. از هر آدرس IP میتوان به برنامه متصل شد.
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- Scope Management -->
|
||||
<v-col cols="12">
|
||||
<v-card variant="outlined" class="pa-4">
|
||||
<v-card-title class="text-subtitle-1 font-weight-medium pb-2">
|
||||
<v-icon start class="mr-2" color="primary">mdi-shield</v-icon>
|
||||
محدودههای دسترسی (Scopes)
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-0">
|
||||
<p class="text-body-2 text-medium-emphasis mb-4">
|
||||
محدودههای دسترسی که این برنامه میتواند از کاربران درخواست کند.
|
||||
</p>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6" v-for="scope in availableScopes" :key="scope.name">
|
||||
<v-checkbox
|
||||
v-model="oauthForm.allowedScopes"
|
||||
:value="scope.name"
|
||||
:label="scope.name"
|
||||
:hint="scope.description"
|
||||
persistent-hint
|
||||
color="primary"
|
||||
>
|
||||
<template v-slot:label>
|
||||
<div>
|
||||
<div class="font-weight-medium">{{ scope.name }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ scope.description }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-alert
|
||||
v-if="oauthForm.allowedScopes.length === 0"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
class="mt-2"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-alert</v-icon>
|
||||
</template>
|
||||
هیچ محدوده دسترسی انتخاب نشده است. این برنامه دسترسی محدودی خواهد داشت.
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-6">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
@click="cancelOAuthEdit"
|
||||
class="mr-2"
|
||||
>
|
||||
انصراف
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="saveOAuthApplication"
|
||||
:loading="loading"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
{{ editingOAuthApp ? 'ویرایش' : 'ایجاد' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Snackbar برای نمایش پیامها -->
|
||||
<v-snackbar
|
||||
v-model="oauthSnackbar.show"
|
||||
:color="oauthSnackbar.color"
|
||||
:timeout="oauthSnackbar.timeout"
|
||||
location="top"
|
||||
>
|
||||
{{ oauthSnackbar.text }}
|
||||
|
||||
<template v-slot:actions>
|
||||
<v-btn
|
||||
color="white"
|
||||
variant="text"
|
||||
@click="oauthSnackbar.show = false"
|
||||
>
|
||||
بستن
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
@ -1144,4 +2044,59 @@ export default defineComponent({
|
|||
.opacity-50 .v-select--disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* استایلهای OAuth */
|
||||
.application-card {
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.application-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.inactive-app {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.inactive-app:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: rgba(var(--v-theme-on-surface), 0.7);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.text-truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
Loading…
Reference in a new issue