Browse Source
feat(api)!: add account exchange-token endpoint
feat(api)!: add account exchange-token endpoint
Introduce ExchangeTokenAPIView at /api/account/exchange-token/ to exchange temporary tokens for a DRF auth token and minimal user profile (id, fullname, email, avatar). Tokens are one-time use and validated via OnlineClassTokenManager. Added serializer, view, URL, and Swagger examples. Update CourseOnlineClassTokenAPIView to return a fixed join URL: https://imamjavad.newhorizonco.uk/join-class?token={TOKEN}&slug={SLUG}. Store course_slug in token payload and remove redirect_path and the _base_components helper. Examples and docs updated. Docs: add CHANGELOG_EXCHANGE_TOKEN.md and exchange_token_api.md; update online_class_entry_flow.md. BREAKING CHANGE: exchange-token endpoint moved from /api/courses/auth/exchange-token/ to /api/account/exchange-token/. Response shape changed: adds token, user.id is numeric, user.name renamed to user.fullname, user.role and user.is_admin removed. Online token response now returns a fixed URL and includes course slug. redirect_path support removed; clients must use the returned token for subsequent requests.master
9 changed files with 669 additions and 21 deletions
-
3apps/account/serializers/__init__.py
-
11apps/account/serializers/auth.py
-
1apps/account/urls.py
-
1apps/account/views/__init__.py
-
126apps/account/views/auth.py
-
25apps/course/views/course.py
-
140docs/CHANGELOG_EXCHANGE_TOKEN.md
-
379docs/exchange_token_api.md
-
4docs/online_class_entry_flow.md
@ -1,3 +1,4 @@ |
|||||
from .user import * |
from .user import * |
||||
from .notification import * |
from .notification import * |
||||
from .location_history import * |
|
||||
|
from .location_history import * |
||||
|
from .auth import * |
||||
@ -0,0 +1,11 @@ |
|||||
|
from rest_framework import serializers |
||||
|
|
||||
|
|
||||
|
class ExchangeTokenSerializer(serializers.Serializer): |
||||
|
temp_token = serializers.CharField(max_length=128) |
||||
|
|
||||
|
def validate_temp_token(self, value: str) -> str: |
||||
|
value = value.strip() |
||||
|
if not value: |
||||
|
raise serializers.ValidationError("temp_token is required.") |
||||
|
return value |
||||
@ -1,5 +1,6 @@ |
|||||
from .user import * |
from .user import * |
||||
from .notification import * |
from .notification import * |
||||
from .location_history import * |
from .location_history import * |
||||
|
from .auth import * |
||||
|
|
||||
|
|
||||
@ -0,0 +1,126 @@ |
|||||
|
import logging |
||||
|
|
||||
|
from django.contrib.auth import get_user_model |
||||
|
|
||||
|
from drf_yasg import openapi |
||||
|
from drf_yasg.utils import swagger_auto_schema |
||||
|
from rest_framework import status |
||||
|
from rest_framework.authtoken.models import Token |
||||
|
from rest_framework.generics import GenericAPIView |
||||
|
from rest_framework.permissions import AllowAny |
||||
|
from rest_framework.response import Response |
||||
|
|
||||
|
from apps.account.serializers import ExchangeTokenSerializer |
||||
|
from utils import absolute_url |
||||
|
from utils.redis import OnlineClassTokenManager |
||||
|
|
||||
|
|
||||
|
logger = logging.getLogger(__name__) |
||||
|
UserModel = get_user_model() |
||||
|
|
||||
|
|
||||
|
class ExchangeTokenAPIView(GenericAPIView): |
||||
|
""" |
||||
|
تبدیل temporary token به اطلاعات کاربر برای ورود از اپ موبایل |
||||
|
""" |
||||
|
permission_classes = [AllowAny] |
||||
|
serializer_class = ExchangeTokenSerializer |
||||
|
|
||||
|
@swagger_auto_schema( |
||||
|
operation_description="Exchange temporary token for user information and authentication token.", |
||||
|
request_body=ExchangeTokenSerializer, |
||||
|
responses={ |
||||
|
status.HTTP_200_OK: openapi.Response( |
||||
|
description="Token exchanged successfully.", |
||||
|
examples={ |
||||
|
"application/json": { |
||||
|
"success": True, |
||||
|
"message": "ورود موفق", |
||||
|
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", |
||||
|
"user": { |
||||
|
"id": 123, |
||||
|
"fullname": "علی احمدی", |
||||
|
"email": "user@example.com", |
||||
|
"avatar": "https://cdn.example.com/avatar.jpg" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
), |
||||
|
status.HTTP_400_BAD_REQUEST: openapi.Response( |
||||
|
description="Invalid request.", |
||||
|
examples={ |
||||
|
"application/json": { |
||||
|
"success": False, |
||||
|
"message": "توکن ارسال نشده است" |
||||
|
} |
||||
|
} |
||||
|
), |
||||
|
status.HTTP_404_NOT_FOUND: openapi.Response( |
||||
|
description="Token not found or expired.", |
||||
|
examples={ |
||||
|
"application/json": { |
||||
|
"success": False, |
||||
|
"message": "توکن نامعتبر یا منقضی شده است" |
||||
|
} |
||||
|
} |
||||
|
), |
||||
|
} |
||||
|
) |
||||
|
def post(self, request, *args, **kwargs): |
||||
|
serializer = self.get_serializer(data=request.data) |
||||
|
serializer.is_valid(raise_exception=True) |
||||
|
|
||||
|
temp_token = serializer.validated_data['temp_token'] |
||||
|
|
||||
|
# دریافت اطلاعات از Redis/Cache |
||||
|
manager = OnlineClassTokenManager() |
||||
|
try: |
||||
|
token_data = manager.get_payload(temp_token) |
||||
|
except Exception: |
||||
|
return Response({ |
||||
|
'success': False, |
||||
|
'message': 'توکن نامعتبر یا منقضی شده است' |
||||
|
}, status=status.HTTP_404_NOT_FOUND) |
||||
|
|
||||
|
user_id = token_data.get('user_id') |
||||
|
if not user_id: |
||||
|
return Response({ |
||||
|
'success': False, |
||||
|
'message': 'توکن نامعتبر است' |
||||
|
}, status=status.HTTP_400_BAD_REQUEST) |
||||
|
|
||||
|
# دریافت کاربر |
||||
|
try: |
||||
|
user = UserModel.objects.get(id=user_id) |
||||
|
except UserModel.DoesNotExist: |
||||
|
return Response({ |
||||
|
'success': False, |
||||
|
'message': 'کاربر یافت نشد' |
||||
|
}, status=status.HTTP_404_NOT_FOUND) |
||||
|
|
||||
|
# حذف توکن موقت (one-time use) |
||||
|
manager.delete_token(temp_token) |
||||
|
|
||||
|
# دریافت یا تولید Token واقعی کاربر |
||||
|
auth_token, _ = Token.objects.get_or_create(user=user) |
||||
|
|
||||
|
# دریافت avatar URL |
||||
|
avatar_url = None |
||||
|
if hasattr(user, 'avatar') and user.avatar: |
||||
|
try: |
||||
|
avatar_url = absolute_url(user.avatar.url) |
||||
|
except Exception: |
||||
|
avatar_url = None |
||||
|
|
||||
|
# برگرداندن اطلاعات کاربر با token واقعی |
||||
|
return Response({ |
||||
|
'success': True, |
||||
|
'message': 'ورود موفق', |
||||
|
'token': auth_token.key, |
||||
|
'user': { |
||||
|
'id': user.id, |
||||
|
'fullname': user.get_full_name() or user.username or '', |
||||
|
'email': user.email or '', |
||||
|
'avatar': avatar_url |
||||
|
} |
||||
|
}, status=status.HTTP_200_OK) |
||||
@ -0,0 +1,140 @@ |
|||||
|
# تغییرات API Exchange Token |
||||
|
|
||||
|
## نسخه جدید (اکتبر 2024) |
||||
|
|
||||
|
### تغییرات اساسی |
||||
|
|
||||
|
#### 1. انتقال API از `courses` به `account` |
||||
|
- **قدیمی**: `/api/courses/auth/exchange-token/` |
||||
|
- **جدید**: `/api/account/exchange-token/` |
||||
|
|
||||
|
#### 2. تغییر ساختار Response |
||||
|
**قدیمی:** |
||||
|
```json |
||||
|
{ |
||||
|
"success": true, |
||||
|
"message": "ورود موفق", |
||||
|
"user": { |
||||
|
"id": "123", |
||||
|
"email": "user@example.com", |
||||
|
"name": "علی احمدی", |
||||
|
"role": "admin", |
||||
|
"is_admin": true |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**جدید:** |
||||
|
```json |
||||
|
{ |
||||
|
"success": true, |
||||
|
"message": "ورود موفق", |
||||
|
"token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0", |
||||
|
"user": { |
||||
|
"id": 123, |
||||
|
"fullname": "علی احمدی", |
||||
|
"email": "user@example.com", |
||||
|
"avatar": "https://cdn.example.com/avatar.jpg" |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### 3. تغییر URL تولید Token موقت |
||||
|
**Response قبلی:** |
||||
|
```json |
||||
|
{ |
||||
|
"token": "abc123", |
||||
|
"url": "https://frontend.example.com/join-class?token=abc123", |
||||
|
"expires_in": 300 |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**Response جدید:** |
||||
|
```json |
||||
|
{ |
||||
|
"token": "abc123xyz789", |
||||
|
"url": "https://imamjavad.newhorizonco.uk/join-class?token=abc123xyz789&slug=python-basics", |
||||
|
"expires_in": 300 |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### تغییرات در فیلدها |
||||
|
|
||||
|
| فیلد قدیمی | فیلد جدید | توضیحات | |
||||
|
|-----------|----------|---------| |
||||
|
| `user.name` | `user.fullname` | تغییر نام فیلد | |
||||
|
| `user.id` (string) | `user.id` (number) | تغییر نوع داده | |
||||
|
| `user.role` | حذف شد | دیگر ارسال نمیشود | |
||||
|
| `user.is_admin` | حذف شد | دیگر ارسال نمیشود | |
||||
|
| - | `token` | **اضافه شد**: توکن احراز هویت DRF | |
||||
|
| - | `user.avatar` | **اضافه شد**: URL تصویر پروفایل | |
||||
|
|
||||
|
### URL ثابت |
||||
|
|
||||
|
دیگر از تنظیمات یا environment variable استفاده نمیشود. URL ثابت است: |
||||
|
|
||||
|
``` |
||||
|
https://imamjavad.newhorizonco.uk/join-class?token={TEMP_TOKEN}&slug={COURSE_SLUG} |
||||
|
``` |
||||
|
|
||||
|
### مزایای تغییرات |
||||
|
|
||||
|
✅ **Token واقعی**: دیگر نیازی به login مجدد نیست |
||||
|
✅ **Avatar**: نمایش تصویر پروفایل کاربر |
||||
|
✅ **Slug دوره**: دسترسی مستقیم به اطلاعات دوره |
||||
|
✅ **URL ثابت**: عدم وابستگی به تنظیمات |
||||
|
✅ **سادهتر**: حذف فیلدهای اضافی (role, is_admin) |
||||
|
|
||||
|
### Migration Guide |
||||
|
|
||||
|
#### Frontend (Next.js) |
||||
|
|
||||
|
**قدیمی:** |
||||
|
```typescript |
||||
|
const user = await api.exchangeToken(tempToken); |
||||
|
localStorage.setItem('user', JSON.stringify(user)); |
||||
|
``` |
||||
|
|
||||
|
**جدید:** |
||||
|
```typescript |
||||
|
const data = await exchangeToken(tempToken); |
||||
|
localStorage.setItem('authToken', data.token); // ⭐ جدید |
||||
|
localStorage.setItem('user', JSON.stringify(data.user)); |
||||
|
``` |
||||
|
|
||||
|
#### Flutter |
||||
|
|
||||
|
**قدیمی:** |
||||
|
```dart |
||||
|
final user = response['user']; |
||||
|
// نیاز به login مجدد داشت |
||||
|
``` |
||||
|
|
||||
|
**جدید:** |
||||
|
```dart |
||||
|
final authToken = response['token']; // ⭐ جدید |
||||
|
final user = response['user']; |
||||
|
// ذخیره token برای درخواستهای بعدی |
||||
|
await storage.write(key: 'authToken', value: authToken); |
||||
|
``` |
||||
|
|
||||
|
### Breaking Changes ⚠️ |
||||
|
|
||||
|
1. **URL تغییر کرد**: باید از `/api/account/exchange-token/` استفاده شود |
||||
|
2. **فیلد `token` اجباری است**: باید در frontend ذخیره و استفاده شود |
||||
|
3. **فیلد `name` به `fullname` تغییر کرد** |
||||
|
4. **فیلدهای `role` و `is_admin` حذف شدند** |
||||
|
5. **URL موقت شامل `slug` میشود** |
||||
|
|
||||
|
### تاریخ اعمال تغییرات |
||||
|
|
||||
|
تاریخ: **14 اکتبر 2024** |
||||
|
|
||||
|
### فایلهای تغییر یافته |
||||
|
|
||||
|
1. `apps/account/serializers/auth.py` (جدید) |
||||
|
2. `apps/account/views/auth.py` (جدید) |
||||
|
3. `apps/account/urls.py` (آپدیت) |
||||
|
4. `apps/course/views/course.py` (آپدیت: CourseOnlineClassTokenAPIView) |
||||
|
5. `docs/exchange_token_api.md` (آپدیت) |
||||
|
6. `docs/online_class_entry_flow.md` (آپدیت) |
||||
@ -0,0 +1,379 @@ |
|||||
|
# API تبدیل توکن موقت (Exchange Token) |
||||
|
|
||||
|
## 📝 توضیحات |
||||
|
|
||||
|
این API برای تبدیل توکن موقت (temporary token) به اطلاعات کاربر واقعی استفاده میشود. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🔗 Endpoint |
||||
|
|
||||
|
``` |
||||
|
POST /api/account/exchange-token/ |
||||
|
``` |
||||
|
|
||||
|
**دسترسی:** عمومی (AllowAny) |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📥 Request Body |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"temp_token": "abc123xyz" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📤 Response |
||||
|
|
||||
|
### ✅ موفق (200 OK) |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"success": true, |
||||
|
"message": "ورود موفق", |
||||
|
"token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0", |
||||
|
"user": { |
||||
|
"id": 123, |
||||
|
"fullname": "علی احمدی", |
||||
|
"email": "user@example.com", |
||||
|
"avatar": "https://cdn.example.com/avatar.jpg" |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**نکته:** |
||||
|
- `token`: توکن احراز هویت واقعی کاربر (DRF Token) که برای درخواستهای بعدی استفاده میشود |
||||
|
- `avatar`: در صورت عدم وجود، مقدار `null` برگردانده میشود |
||||
|
|
||||
|
### ❌ خطا - توکن نامعتبر یا منقضی (404 NOT FOUND) |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"success": false, |
||||
|
"message": "توکن نامعتبر یا منقضی شده است" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### ❌ خطا - کاربر یافت نشد (404 NOT FOUND) |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"success": false, |
||||
|
"message": "کاربر یافت نشد" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### ❌ خطا - توکن ارسال نشده (400 BAD REQUEST) |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"success": false, |
||||
|
"message": "توکن نامعتبر است" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🧪 تست با curl |
||||
|
|
||||
|
### مرحله 1: تولید توکن موقت |
||||
|
|
||||
|
```bash |
||||
|
curl -X POST http://localhost:8000/api/courses/1/online/token/ \ |
||||
|
-H "Authorization: Token <USER_BACKEND_TOKEN>" \ |
||||
|
-H "Content-Type: application/json" |
||||
|
``` |
||||
|
|
||||
|
**Response:** |
||||
|
```json |
||||
|
{ |
||||
|
"token": "abc123xyz789...", |
||||
|
"url": "https://imamjavad.newhorizonco.uk/join-class?token=abc123xyz789...&slug=python-basics", |
||||
|
"expires_in": 300 |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**نکته:** URL ثابت است و شامل `token` و `slug` دوره میباشد. |
||||
|
|
||||
|
### مرحله 2: تبدیل توکن موقت |
||||
|
|
||||
|
```bash |
||||
|
curl -X POST http://localhost:8000/api/account/exchange-token/ \ |
||||
|
-H "Content-Type: application/json" \ |
||||
|
-d '{"temp_token": "abc123xyz"}' |
||||
|
``` |
||||
|
|
||||
|
**Response:** |
||||
|
```json |
||||
|
{ |
||||
|
"success": true, |
||||
|
"message": "ورود موفق", |
||||
|
"token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0", |
||||
|
"user": { |
||||
|
"id": 123, |
||||
|
"fullname": "علی احمدی", |
||||
|
"email": "user@example.com", |
||||
|
"avatar": "https://cdn.example.com/avatar.jpg" |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🔐 امنیت |
||||
|
|
||||
|
1. **One-Time Use**: توکن بعد از استفاده حذف میشود |
||||
|
2. **TTL**: توکن پس از 300 ثانیه (5 دقیقه) منقضی میشود |
||||
|
3. **Role Detection**: نقش کاربر (admin/user) بر اساس course و permissions تشخیص داده میشود |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📋 فایلهای پیادهسازی شده |
||||
|
|
||||
|
1. **Serializer**: `/apps/account/serializers/auth.py` |
||||
|
- اضافه شد: `ExchangeTokenSerializer` |
||||
|
|
||||
|
2. **View**: `/apps/account/views/auth.py` |
||||
|
- اضافه شد: `ExchangeTokenAPIView` |
||||
|
|
||||
|
3. **URL**: `/apps/account/urls.py` |
||||
|
- اضافه شد: `path('exchange-token/', ...)` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🔄 فلوی کامل |
||||
|
|
||||
|
``` |
||||
|
📱 Mobile App |
||||
|
↓ |
||||
|
POST /api/courses/1/online/token/ |
||||
|
Headers: Authorization: Token <USER_TOKEN> |
||||
|
↓ |
||||
|
Response: { |
||||
|
"token": "abc123", |
||||
|
"url": "https://imamjavad.newhorizonco.uk/join-class?token=abc123&slug=python-basics" |
||||
|
} |
||||
|
↓ |
||||
|
📱 Open URL in Browser |
||||
|
↓ |
||||
|
🌐 Frontend: /join-class?token=abc123&slug=python-basics |
||||
|
↓ |
||||
|
POST /api/account/exchange-token/ |
||||
|
Body: {"temp_token": "abc123"} |
||||
|
↓ |
||||
|
Response: { |
||||
|
"token": "a1b2c3...", |
||||
|
"user": {...} |
||||
|
} |
||||
|
↓ |
||||
|
Save token + user in localStorage |
||||
|
↓ |
||||
|
Redirect to /online-classroom (with slug if needed) |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## ✨ ویژگیها |
||||
|
|
||||
|
✅ **Token Management**: استفاده از Redis برای ذخیرهسازی موقت |
||||
|
✅ **Auth Token**: برگرداندن DRF Token واقعی برای احراز هویت |
||||
|
✅ **Security**: One-time use و TTL محدود |
||||
|
✅ **Error Handling**: مدیریت کامل خطاها |
||||
|
✅ **Avatar Support**: برگرداندن URL avatar کاربر |
||||
|
✅ **Documentation**: Swagger/OpenAPI documentation |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🛠️ نکات پیادهسازی |
||||
|
|
||||
|
1. از `OnlineClassTokenManager` استفاده میشود که در `utils/redis.py` تعریف شده |
||||
|
2. توکن موقت پس از استفاده با `manager.delete_token()` حذف میشود |
||||
|
3. نقش کاربر با بررسی `user.can_manage_course(course)` تشخیص داده میشود |
||||
|
4. در صورت عدم وجود course_id، فقط `user.is_staff` بررسی میشود |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📱 مثال Flutter |
||||
|
|
||||
|
```dart |
||||
|
import 'package:http/http.dart' as http; |
||||
|
import 'dart:convert'; |
||||
|
import 'package:url_launcher/url_launcher.dart'; |
||||
|
|
||||
|
class OnlineClassService { |
||||
|
final String baseUrl = 'https://your-backend.com'; |
||||
|
|
||||
|
/// مرحله 1: دریافت توکن موقت از بکند |
||||
|
Future<Map<String, dynamic>> getTemporaryToken({ |
||||
|
required int courseId, |
||||
|
required String userToken, |
||||
|
}) async { |
||||
|
final response = await http.post( |
||||
|
Uri.parse('$baseUrl/courses/$courseId/online/token/'), |
||||
|
headers: { |
||||
|
'Authorization': 'Token $userToken', |
||||
|
'Content-Type': 'application/json', |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
if (response.statusCode == 201) { |
||||
|
return jsonDecode(response.body); |
||||
|
} else { |
||||
|
throw Exception('Failed to get temporary token'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// مرحله 2: باز کردن لینک در مرورگر |
||||
|
Future<void> openOnlineClass({ |
||||
|
required int courseId, |
||||
|
required String userToken, |
||||
|
}) async { |
||||
|
try { |
||||
|
// دریافت توکن موقت |
||||
|
final tokenData = await getTemporaryToken( |
||||
|
courseId: courseId, |
||||
|
userToken: userToken, |
||||
|
); |
||||
|
|
||||
|
final joinUrl = tokenData['url'] as String; |
||||
|
|
||||
|
// باز کردن در مرورگر |
||||
|
final uri = Uri.parse(joinUrl); |
||||
|
if (await canLaunchUrl(uri)) { |
||||
|
await launchUrl( |
||||
|
uri, |
||||
|
mode: LaunchMode.externalApplication, |
||||
|
); |
||||
|
} else { |
||||
|
throw Exception('Could not launch $joinUrl'); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
print('Error opening online class: $e'); |
||||
|
rethrow; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// استفاده در UI |
||||
|
class CourseDetailScreen extends StatelessWidget { |
||||
|
final int courseId; |
||||
|
final String userToken; |
||||
|
final OnlineClassService _service = OnlineClassService(); |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return Scaffold( |
||||
|
body: Center( |
||||
|
child: ElevatedButton( |
||||
|
onPressed: () async { |
||||
|
try { |
||||
|
await _service.openOnlineClass( |
||||
|
courseId: courseId, |
||||
|
userToken: userToken, |
||||
|
); |
||||
|
} catch (e) { |
||||
|
ScaffoldMessenger.of(context).showSnackBar( |
||||
|
SnackBar(content: Text('خطا در ورود به کلاس: $e')), |
||||
|
); |
||||
|
} |
||||
|
}, |
||||
|
child: Text('ورود به کلاس آنلاین'), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### نصب dependencies برای Flutter |
||||
|
|
||||
|
در `pubspec.yaml`: |
||||
|
|
||||
|
```yaml |
||||
|
dependencies: |
||||
|
http: ^1.1.0 |
||||
|
url_launcher: ^6.2.1 |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🌐 مثال Frontend (Next.js) |
||||
|
|
||||
|
صفحه `/app/join-class/page.tsx`: |
||||
|
|
||||
|
```typescript |
||||
|
'use client'; |
||||
|
|
||||
|
import { useEffect, useState } from 'react'; |
||||
|
import { useRouter, useSearchParams } from 'next/navigation'; |
||||
|
import { api } from '@/lib/api'; |
||||
|
|
||||
|
export default function JoinClassPage() { |
||||
|
const router = useRouter(); |
||||
|
const searchParams = useSearchParams(); |
||||
|
const [error, setError] = useState<string | null>(null); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const token = searchParams.get('token'); |
||||
|
|
||||
|
if (!token) { |
||||
|
setError('توکن یافت نشد'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// تبدیل توکن به اطلاعات کاربر |
||||
|
exchangeToken(token) |
||||
|
.then((data) => { |
||||
|
// ذخیره token و user در localStorage |
||||
|
localStorage.setItem('authToken', data.token); |
||||
|
localStorage.setItem('user', JSON.stringify(data.user)); |
||||
|
|
||||
|
// هدایت به صفحه کلاس |
||||
|
router.push('/online-classroom'); |
||||
|
}) |
||||
|
.catch((err) => { |
||||
|
setError(err.message || 'خطا در ورود'); |
||||
|
}); |
||||
|
}, [searchParams, router]); |
||||
|
|
||||
|
async function exchangeToken(tempToken: string) { |
||||
|
const response = await fetch( |
||||
|
`${process.env.NEXT_PUBLIC_API_URL}/api/account/exchange-token/`, |
||||
|
{ |
||||
|
method: 'POST', |
||||
|
headers: { |
||||
|
'Content-Type': 'application/json', |
||||
|
}, |
||||
|
body: JSON.stringify({ temp_token: tempToken }), |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
if (!response.ok) { |
||||
|
const error = await response.json(); |
||||
|
throw new Error(error.message || 'Failed to exchange token'); |
||||
|
} |
||||
|
|
||||
|
const data = await response.json(); |
||||
|
return data; // Returns { success, message, token, user } |
||||
|
} |
||||
|
|
||||
|
if (error) { |
||||
|
return ( |
||||
|
<div className="flex items-center justify-center min-h-screen"> |
||||
|
<div className="text-red-500">{error}</div> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div className="flex items-center justify-center min-h-screen"> |
||||
|
<div>در حال ورود...</div> |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue