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

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"
  }
}

🔐 امنیت

  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

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>
  );
}