# 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 " \ -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 ↓ 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> 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 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(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 (
{error}
); } return (
در حال ورود...
); } ```