You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
8.9 KiB
8.9 KiB
API تبدیل توکن موقت (Exchange Token)
📝 توضیحات
این API برای تبدیل توکن موقت (temporary token) به اطلاعات کاربر واقعی استفاده میشود.
🔗 Endpoint
POST /api/account/exchange-token/
دسترسی: عمومی (AllowAny)
📥 Request Body
{
"temp_token": "abc123xyz"
}
📤 Response
✅ موفق (200 OK)
{
"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)
{
"success": false,
"message": "توکن نامعتبر یا منقضی شده است"
}
❌ خطا - کاربر یافت نشد (404 NOT FOUND)
{
"success": false,
"message": "کاربر یافت نشد"
}
❌ خطا - توکن ارسال نشده (400 BAD REQUEST)
{
"success": false,
"message": "توکن نامعتبر است"
}
🧪 تست با curl
مرحله 1: تولید توکن موقت
curl -X POST http://localhost:8000/api/courses/1/online/token/ \
-H "Authorization: Token <USER_BACKEND_TOKEN>" \
-H "Content-Type: application/json"
Response:
{
"token": "abc123xyz789...",
"url": "https://imamjavad.newhorizonco.uk/join-class?token=abc123xyz789...&slug=python-basics",
"expires_in": 300
}
نکته: URL ثابت است و شامل token و slug دوره میباشد.
مرحله 2: تبدیل توکن موقت
curl -X POST http://localhost:8000/api/account/exchange-token/ \
-H "Content-Type: application/json" \
-d '{"temp_token": "abc123xyz"}'
Response:
{
"success": true,
"message": "ورود موفق",
"token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
"user": {
"id": 123,
"fullname": "علی احمدی",
"email": "user@example.com",
"avatar": "https://cdn.example.com/avatar.jpg"
}
}
🔐 امنیت
- One-Time Use: توکن بعد از استفاده حذف میشود
- TTL: توکن پس از 300 ثانیه (5 دقیقه) منقضی میشود
- Role Detection: نقش کاربر (admin/user) بر اساس course و permissions تشخیص داده میشود
📋 فایلهای پیادهسازی شده
-
Serializer:
/apps/account/serializers/auth.py- اضافه شد:
ExchangeTokenSerializer
- اضافه شد:
-
View:
/apps/account/views/auth.py- اضافه شد:
ExchangeTokenAPIView
- اضافه شد:
-
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
🛠️ نکات پیادهسازی
- از
OnlineClassTokenManagerاستفاده میشود که درutils/redis.pyتعریف شده - توکن موقت پس از استفاده با
manager.delete_token()حذف میشود - نقش کاربر با بررسی
user.can_manage_course(course)تشخیص داده میشود - در صورت عدم وجود course_id، فقط
user.is_staffبررسی میشود
📱 مثال Flutter
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:
dependencies:
http: ^1.1.0
url_launcher: ^6.2.1
🌐 مثال Frontend (Next.js)
صفحه /app/join-class/page.tsx:
'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>
);
}