Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/adapters/api/v1/users.py
2026-07-17 17:17:06 +00:00

1789 lines
60 KiB
Python
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Removed __future__ annotations to fix OpenAPI schema generation
from fastapi import APIRouter, Depends, Request, Query, UploadFile, File, Body
from sqlalchemy.orm import Session
import io
from adapters.db.session import get_db
from adapters.db.repositories.user_repo import UserRepository
from adapters.db.repositories.password_reset_repo import PasswordResetRepository
from adapters.api.v1.schemas import (
QueryInfo, SuccessResponse, UsersListResponse, UsersSummaryResponse, UserResponse,
BulkActivateRequest, BulkSuspendRequest, BulkResetPasswordRequest,
UserDetailResponse, BulkOperationResponse, BulkResetPasswordResponse,
AdminSetUserPasswordRequest,
)
from app.core.responses import success_response, format_datetime_fields, ApiError
from app.core.auth_dependency import get_current_user, AuthContext
from app.core.permissions import require_user_management
from app.core.cache import get_cache
from app.services.file_storage_service import FileStorageService
from app.services.auth_service import (
_hash_reset_token,
send_password_reset_notification,
admin_set_user_password,
build_password_reset_web_url,
)
from app.core.settings import get_settings
from secrets import token_urlsafe
from datetime import datetime, timedelta, date, time
from typing import Literal
from uuid import UUID
from starlette.responses import StreamingResponse
router = APIRouter(prefix="/users", tags=["کاربران", "مدیریت سیستم"])
def verify_user_management_page_access(ctx: AuthContext = Depends(get_current_user)) -> AuthContext:
"""همان بازهٔ دسترسی UI: سوپرادمین یا system_settings یا user_management."""
if ctx.is_superadmin() or ctx.has_app_permission("system_settings") or ctx.has_app_permission("user_management"):
return ctx
raise ApiError("FORBIDDEN", "مجوز دسترسی به مدیریت کاربران یا تنظیمات سیستم لازم است", http_status=403)
@router.post("/search",
summary="لیست کاربران با فیلتر پیشرفته",
description="""
دریافت لیست کاربران با قابلیت فیلتر، جستجو، مرتب‌سازی و صفحه‌بندی.
### نکات:
- نیاز به سوپرادمین یا مجوز `system_settings` یا `user_management` دارد
- Rate Limit: 500 request در دقیقه (عمومی برای تمام endpoint ها)
- نتایج به مدت 60 ثانیه cache می‌شوند (Cache key بر اساس query parameters و user_id ایجاد می‌شود)
- برای جستجوی ساده از `GET /users` استفاده کنید
- حداکثر تعداد رکورد در هر صفحه: 1000 (پارامتر `take`)
""",
response_model=SuccessResponse,
responses={
200: {
"description": "لیست کاربران با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "لیست کاربران دریافت شد",
"data": {
"items": [
{
"id": 1,
"email": "user@example.com",
"mobile": "09123456789",
"first_name": "احمد",
"last_name": "احمدی",
"is_active": True,
"referral_code": "ABC123",
"created_at": "2024-01-01T00:00:00Z"
}
],
"pagination": {
"total": 1,
"page": 1,
"per_page": 10,
"total_pages": 1,
"has_next": False,
"has_prev": False
}
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز - نیاز به مجوز usermanager",
"content": {
"application/json": {
"example": {
"success": False,
"message": "Missing app permission: user_management",
"error_code": "FORBIDDEN"
}
}
}
},
422: {
"description": "خطا در اعتبارسنجی داده‌ها",
"content": {
"application/json": {
"example": {
"success": False,
"message": "take نمی‌تواند بیشتر از 1000 باشد",
"error_code": "VALIDATION_ERROR"
}
}
}
},
429: {
"description": "تعداد درخواست‌ها بیش از حد مجاز",
"content": {
"application/json": {
"example": {
"success": False,
"message": "تعداد درخواست‌های شما بیش از حد مجاز است. لطفاً کمی صبر کنید.",
"error_code": "RATE_LIMIT_EXCEEDED"
}
}
},
"headers": {
"X-RateLimit-Limit": {"description": "حد مجاز درخواست", "schema": {"type": "integer", "example": 500}},
"X-RateLimit-Remaining": {"description": "تعداد درخواست‌های باقیمانده", "schema": {"type": "integer", "example": 0}},
"X-RateLimit-Reset": {"description": "زمان reset به ثانیه (Unix timestamp)", "schema": {"type": "integer", "example": 1705324800}},
"Retry-After": {"description": "زمان باقیمانده تا retry به ثانیه", "schema": {"type": "integer", "example": 30}}
}
}
}
)
def list_users(
request: Request,
query_info: QueryInfo = Body(..., description="پارامترهای جستجو، فیلتر، مرتب‌سازی و صفحه‌بندی"),
ctx: AuthContext = Depends(verify_user_management_page_access),
db: Session = Depends(get_db)
):
"""
دریافت لیست کاربران با قابلیت فیلتر، جستجو، مرتب‌سازی و صفحه‌بندی
### پارامترهای QueryInfo:
- **sort_by**: فیلد مرتب‌سازی (مثال: created_at, first_name, email)
- **sort_desc**: ترتیب نزولی (true/false)
- **take**: تعداد رکورد در هر صفحه (پیش‌فرض: 10، حداکثر: 100)
- **skip**: تعداد رکورد صرف‌نظر شده (پیش‌فرض: 0)
- **search**: عبارت جستجو (جستجو در فیلدهای مشخص شده)
- **search_fields**: فیلدهای جستجو (مثال: ["first_name", "last_name", "email"])
- **filters**: آرایه فیلترها با ساختار:
```json
[
{
"property": "is_active",
"operator": "=",
"value": true
},
{
"property": "first_name",
"operator": "*",
"value": "احمد"
},
{
"property": "status",
"operator": "in",
"value": ["active", "inactive"]
},
{
"property": "role",
"operator": "=",
"value": "admin"
}
]
```
### عملگرهای پشتیبانی شده:
- **=**: برابر
- **>**: بزرگتر از
- **>=**: بزرگتر یا مساوی
- **<**: کوچکتر از
- **<=**: کوچکتر یا مساوی
- **!=**: نامساوی
- **\\***: شامل (contains)
- **?\\***: خاتمه یابد (ends with)
- **\\*?**: شروع شود (starts with)
- **in**: در بین مقادیر آرایه
- **not_in**: موجود نیست در لیست
- **is_null**: مقدار خالی است
- **is_not_null**: مقدار خالی نیست
### فیلترهای خاص:
- **status**: می‌تواند "active", "inactive", "suspended" باشد (به صورت خودکار به is_active تبدیل می‌شود)
- **role**: می‌تواند "user", "admin", "operator", "supervisor" باشد (بر اساس app_permissions)
### Cache:
- نتایج به مدت 60 ثانیه cache می‌شوند
- Cache key بر اساس query parameters و user_id ایجاد می‌شود
### مثال cURL:
```bash
curl -X POST "http://localhost:8000/api/v1/users/search" \\
-H "Authorization: Bearer sk_your_api_key" \\
-H "Content-Type: application/json" \\
-d '{
"take": 20,
"skip": 0,
"sort_by": "created_at",
"sort_desc": true,
"search": "احمد",
"search_fields": ["first_name", "last_name", "email"],
"filters": [
{
"property": "is_active",
"operator": "=",
"value": true
}
]
}'
```
"""
repo = UserRepository(db)
# کش لیست کاربران مدیریت‌شده (قبل از کوئری DB)
cache = get_cache()
cache_key = None
if cache.enabled:
import json, hashlib
key_payload = {
"admin_user_id": ctx.get_user_id(),
"query": {
"sort_by": query_info.sort_by,
"sort_desc": query_info.sort_desc,
"take": query_info.take,
"skip": query_info.skip,
"search": query_info.search,
"search_fields": query_info.search_fields,
"filters": [f.model_dump() for f in (query_info.filters or [])],
},
}
key_str = json.dumps(key_payload, sort_keys=True, ensure_ascii=False)
key_hash = hashlib.sha256(key_str.encode("utf-8")).hexdigest()[:16]
cache_key = f"users_list:{key_hash}"
cached = cache.get(cache_key)
if cached is not None:
return success_response(cached, request)
users, total = repo.query_admin_list(query_info)
# تبدیل User objects به dictionary با اطلاعات اضافی
user_dicts = [repo.to_dict(user, include_extended=True) for user in users]
# فرمت کردن تاریخ‌ها
formatted_users = [format_datetime_fields(user_dict, request) for user_dict in user_dicts]
# محاسبه اطلاعات صفحه‌بندی
page = (query_info.skip // query_info.take) + 1
total_pages = (total + query_info.take - 1) // query_info.take
response_data = {
"items": formatted_users,
"pagination": {
"total": total,
"page": page,
"per_page": query_info.take,
"total_pages": total_pages,
"has_next": page < total_pages,
"has_prev": page > 1
},
"query_info": {
"sort_by": query_info.sort_by,
"sort_desc": query_info.sort_desc,
"search": query_info.search,
"search_fields": query_info.search_fields,
"filters": [{"property": f.property, "operator": f.operator, "value": f.value} for f in (query_info.filters or [])]
}
}
if cache.enabled and cache_key:
cache.set(cache_key, response_data, ttl=60)
return success_response(response_data, request)
@router.get("",
summary="لیست ساده کاربران",
description="""
دریافت لیست ساده کاربران با صفحه‌بندی.
### Query Parameters:
- **limit**: تعداد رکورد در هر صفحه (پیش‌فرض: 10، حداقل: 1، حداکثر: 100)
- **offset**: تعداد رکورد صرف‌نظر شده (پیش‌فرض: 0، حداقل: 0)
### نکات:
- این endpoint برای دریافت لیست ساده کاربران بدون فیلتر پیشرفته است
- برای جستجو و فیلتر پیشرفته از `POST /users/search` استفاده کنید
- نیاز به مجوز `user_management` در سطح اپلیکیشن دارد
- Rate Limit: 500 request در دقیقه (عمومی برای تمام endpoint ها)
- حداکثر تعداد رکورد در هر صفحه: 100 (پارامتر `limit`)
### مثال cURL:
```bash
curl -X GET "http://localhost:8000/api/v1/users?limit=20&offset=0" \\
-H "Authorization: Bearer sk_your_api_key"
```
""",
response_model=SuccessResponse,
responses={
200: {
"description": "لیست کاربران با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "لیست کاربران دریافت شد",
"data": [
{
"id": 1,
"email": "user@example.com",
"mobile": "09123456789",
"first_name": "احمد",
"last_name": "احمدی",
"is_active": True,
"referral_code": "ABC123",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است",
"content": {
"application/json": {
"example": {
"success": False,
"message": "Unauthorized",
"error_code": "UNAUTHORIZED"
}
}
}
},
403: {
"description": "دسترسی غیرمجاز - نیاز به مجوز user_management",
"content": {
"application/json": {
"example": {
"success": False,
"message": "Missing app permission: user_management",
"error_code": "FORBIDDEN"
}
}
}
},
422: {
"description": "خطا در اعتبارسنجی query parameters",
"content": {
"application/json": {
"example": {
"success": False,
"message": "limit must be between 1 and 100",
"error_code": "VALIDATION_ERROR"
}
}
}
},
429: {
"description": "تعداد درخواست‌ها بیش از حد مجاز",
"content": {
"application/json": {
"example": {
"success": False,
"message": "تعداد درخواست‌های شما بیش از حد مجاز است. لطفاً کمی صبر کنید.",
"error_code": "RATE_LIMIT_EXCEEDED"
}
}
},
"headers": {
"X-RateLimit-Limit": {"description": "حد مجاز درخواست", "schema": {"type": "integer", "example": 500}},
"X-RateLimit-Remaining": {"description": "تعداد درخواست‌های باقیمانده", "schema": {"type": "integer", "example": 0}},
"X-RateLimit-Reset": {"description": "زمان reset به ثانیه (Unix timestamp)", "schema": {"type": "integer", "example": 1705324800}},
"Retry-After": {"description": "زمان باقیمانده تا retry به ثانیه", "schema": {"type": "integer", "example": 30}}
}
}
}
)
@require_user_management()
def list_users_simple(
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
limit: int = Query(10, ge=1, le=100, description="تعداد رکورد در هر صفحه (حداقل: 1، حداکثر: 100)"),
offset: int = Query(0, ge=0, description="تعداد رکورد صرف‌نظر شده (حداقل: 0)")
):
"""دریافت لیست ساده کاربران"""
repo = UserRepository(db)
# Create basic query info
query_info = QueryInfo(take=limit, skip=offset)
users, total = repo.query_with_filters(query_info)
# تبدیل User objects به dictionary
user_dicts = [repo.to_dict(user) for user in users]
# فرمت کردن تاریخ‌ها
formatted_users = [format_datetime_fields(user_dict, None) for user_dict in user_dicts]
return success_response(formatted_users, None)
@router.post(
"/me/signature",
summary="آپلود امضای کاربر جاری",
description="""
آپلود تصویر امضای کاربر و ذخیره آن در سیستم فایل.
### محدودیت‌های فایل:
- **فرمت‌های مجاز**: JPG, JPEG, PNG, GIF, WebP, BMP
- **MIME Types**: image/jpeg, image/png, image/gif, image/webp, image/bmp
- **حداکثر حجم**: بر اساس تنظیمات سیستم (پیش‌فرض: 10 مگابایت)
- **ابعاد تصویر**: محدودیت خاصی ندارد (توصیه می‌شود برای بهینه‌سازی حداکثر 2000x2000 پیکسل)
- **نسبت تصویر**: محدودیت خاصی ندارد
- **انقضا**: فایل به مدت 10 سال (3650 روز) نگهداری می‌شود
### مثال cURL:
```bash
curl -X POST "http://localhost:8000/api/v1/users/me/signature" \\
-H "Authorization: Bearer sk_your_api_key" \\
-F "file=@signature.png"
```
""",
response_model=SuccessResponse,
responses={
200: {
"description": "امضا با موفقیت آپلود شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "امضا با موفقیت آپلود شد",
"data": {
"signature_file_id": "550e8400-e29b-41d4-a716-446655440000",
"file": {
"file_id": "550e8400-e29b-41d4-a716-446655440000",
"filename": "signature.png",
"mime_type": "image/png",
"file_size": 245678,
"uploaded_at": "2024-01-15T10:30:00Z"
}
}
}
}
}
},
400: {
"description": "خطا در آپلود فایل",
"content": {
"application/json": {
"example": {
"success": False,
"message": "فرمت فایل معتبر نیست. فقط فرمت‌های JPG, PNG, GIF, WebP و BMP پشتیبانی می‌شوند",
"error_code": "INVALID_FILE_FORMAT"
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
404: {
"description": "کاربر یافت نشد",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کاربر یافت نشد",
"error_code": "USER_NOT_FOUND"
}
}
}
},
413: {
"description": "حجم فایل بیش از حد مجاز",
"content": {
"application/json": {
"example": {
"success": False,
"message": "حجم فایل از حداکثر مجاز (10 مگابایت) تجاوز می‌کند",
"error_code": "FILE_SIZE_EXCEEDED",
"file_size_mb": 15.5,
"max_file_size_mb": 10
}
}
}
},
429: {
"description": "تعداد درخواست‌ها بیش از حد مجاز",
"content": {
"application/json": {
"example": {
"success": False,
"message": "تعداد درخواست‌های شما بیش از حد مجاز است. لطفاً کمی صبر کنید.",
"error_code": "RATE_LIMIT_EXCEEDED"
}
}
},
"headers": {
"X-RateLimit-Limit": {"description": "حد مجاز درخواست", "schema": {"type": "integer", "example": 500}},
"X-RateLimit-Remaining": {"description": "تعداد درخواست‌های باقیمانده", "schema": {"type": "integer", "example": 0}},
"X-RateLimit-Reset": {"description": "زمان reset به ثانیه (Unix timestamp)", "schema": {"type": "integer", "example": 1705324800}},
"Retry-After": {"description": "زمان باقیمانده تا retry به ثانیه", "schema": {"type": "integer", "example": 30}}
}
}
}
)
async def upload_my_signature(
request: Request,
file: UploadFile = File(...),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""آپلود امضای کاربر کنونی و ذخیره file_id در User.signature_file_id"""
repo = UserRepository(db)
user = repo.get_by_id(ctx.get_user_id())
if not user:
from app.core.responses import ApiError
raise ApiError("USER_NOT_FOUND", "کاربر یافت نشد", http_status=404)
storage = FileStorageService(db)
saved = await storage.upload_file(
file=file,
user_id=ctx.get_user_id(),
module_context="user_signature",
context_id=None,
developer_data={"user_id": ctx.get_user_id()},
is_temporary=False,
expires_in_days=3650,
)
# به‌روز کردن شناسه امضا روی کاربر
user.signature_file_id = saved.get("file_id")
db.commit()
return success_response(
{
"signature_file_id": user.signature_file_id,
"file": saved,
},
request,
)
@router.get(
"/me/signature",
summary="دریافت فایل امضای کاربر جاری",
description="""
بازگرداندن تصویر امضای کاربر کنونی به‌صورت فایل (برای نمایش در پروفایل یا فاکتور).
### مثال cURL:
```bash
curl -X GET "http://localhost:8000/api/v1/users/me/signature" \\
-H "Authorization: Bearer sk_your_api_key" \\
--output signature.png
```
### Response:
- Content-Type: image/png (یا فرمت فایل آپلود شده)
- Content-Disposition: inline; filename="signature.png"
""",
responses={
200: {
"description": "فایل امضا با موفقیت دریافت شد",
"content": {
"image/png": {
"schema": {
"type": "string",
"format": "binary"
}
},
"image/jpeg": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
404: {
"description": "امضایی برای این کاربر ثبت نشده است",
"content": {
"application/json": {
"example": {
"success": False,
"message": "امضایی برای این کاربر ثبت نشده است",
"error_code": "SIGNATURE_NOT_SET"
}
}
}
},
429: {
"description": "تعداد درخواست‌ها بیش از حد مجاز",
"content": {
"application/json": {
"example": {
"success": False,
"message": "تعداد درخواست‌های شما بیش از حد مجاز است. لطفاً کمی صبر کنید.",
"error_code": "RATE_LIMIT_EXCEEDED"
}
}
},
"headers": {
"X-RateLimit-Limit": {"description": "حد مجاز درخواست", "schema": {"type": "integer", "example": 500}},
"X-RateLimit-Remaining": {"description": "تعداد درخواست‌های باقیمانده", "schema": {"type": "integer", "example": 0}},
"X-RateLimit-Reset": {"description": "زمان reset به ثانیه (Unix timestamp)", "schema": {"type": "integer", "example": 1705324800}},
"Retry-After": {"description": "زمان باقیمانده تا retry به ثانیه", "schema": {"type": "integer", "example": 30}}
}
}
}
)
async def get_my_signature(
request: Request,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
):
repo = UserRepository(db)
user = repo.get_by_id(ctx.get_user_id())
if not user or not getattr(user, "signature_file_id", None):
from app.core.responses import ApiError
raise ApiError("SIGNATURE_NOT_SET", "امضایی برای این کاربر ثبت نشده است", http_status=404)
storage = FileStorageService(db)
file_data = await storage.download_file(UUID(str(user.signature_file_id)))
return StreamingResponse(
content=io.BytesIO(file_data["content"]),
media_type=file_data["mime_type"] or "image/png",
headers={"Content-Disposition": f'inline; filename="{file_data["filename"]}"'},
)
@router.get("/{user_id}",
summary="دریافت اطلاعات یک کاربر",
description="""
دریافت اطلاعات کامل یک کاربر بر اساس شناسه شامل:
- اطلاعات پایه کاربر (ایمیل، موبایل، نام و ...)
- لیست کسب‌وکارهای کاربر (مالک یا عضو)
- نشست‌های فعال کاربر
- آخرین فعالیت‌های کاربر (حداکثر 50 مورد)
### نکات:
- نیاز به مجوز `user_management` در سطح اپلیکیشن دارد
- Rate Limit: 500 request در دقیقه (عمومی برای تمام endpoint ها)
### مثال cURL:
```bash
curl -X GET "http://localhost:8000/api/v1/users/123" \\
-H "Authorization: Bearer sk_your_api_key"
```
""",
response_model=SuccessResponse,
responses={
200: {
"description": "اطلاعات کاربر با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "اطلاعات کاربر دریافت شد",
"data": {
"id": 1,
"email": "user@example.com",
"mobile": "09123456789",
"first_name": "احمد",
"last_name": "احمدی",
"is_active": True,
"referral_code": "ABC123",
"app_permissions": {"user_management": True},
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
"signature_file_id": "550e8400-e29b-41d4-a716-446655440000",
"businesses": [
{
"id": 1,
"name": "شرکت نمونه",
"field": "بازرگانی",
"role": "owner",
"status": "active",
"created_at": "2024-01-01T00:00:00Z"
},
{
"id": 2,
"name": "فروشگاه نمونه",
"field": "خدماتی",
"role": "admin",
"status": "active",
"created_at": "2024-01-05T00:00:00Z"
}
],
"sessions": [
{
"id": 1,
"device": "Chrome on Windows",
"ip": "192.168.1.1",
"last_active_at": "2024-01-15T10:30:00Z",
"created_at": "2024-01-01T00:00:00Z"
},
{
"id": 2,
"device": "Firefox on Linux",
"ip": "192.168.1.2",
"last_active_at": "2024-01-14T15:20:00Z",
"created_at": "2024-01-10T00:00:00Z"
}
],
"audit_logs": [
{
"id": 1,
"action": "login",
"description": "ورود به سیستم",
"category": "authentication",
"entity_type": "user",
"entity_id": 1,
"created_at": "2024-01-15T10:30:00Z"
},
{
"id": 2,
"action": "update_profile",
"description": "به‌روزرسانی پروفایل",
"category": "profile",
"entity_type": "user",
"entity_id": 1,
"created_at": "2024-01-14T09:15:00Z"
}
]
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است",
"content": {
"application/json": {
"example": {
"success": False,
"message": "Unauthorized",
"error_code": "UNAUTHORIZED"
}
}
}
},
403: {
"description": "دسترسی غیرمجاز - نیاز به مجوز user_management",
"content": {
"application/json": {
"example": {
"success": False,
"message": "Missing app permission: user_management",
"error_code": "FORBIDDEN"
}
}
}
},
404: {
"description": "کاربر یافت نشد",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کاربر یافت نشد",
"error_code": "USER_NOT_FOUND"
}
}
}
},
429: {
"description": "تعداد درخواست‌ها بیش از حد مجاز",
"content": {
"application/json": {
"example": {
"success": False,
"message": "تعداد درخواست‌های شما بیش از حد مجاز است. لطفاً کمی صبر کنید.",
"error_code": "RATE_LIMIT_EXCEEDED"
}
}
},
"headers": {
"X-RateLimit-Limit": {"description": "حد مجاز درخواست", "schema": {"type": "integer", "example": 500}},
"X-RateLimit-Remaining": {"description": "تعداد درخواست‌های باقیمانده", "schema": {"type": "integer", "example": 0}},
"X-RateLimit-Reset": {"description": "زمان reset به ثانیه (Unix timestamp)", "schema": {"type": "integer", "example": 1705324800}},
"Retry-After": {"description": "زمان باقیمانده تا retry به ثانیه", "schema": {"type": "integer", "example": 30}}
}
}
}
)
@require_user_management()
def get_user(
user_id: int,
request: Request,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""دریافت اطلاعات کامل یک کاربر بر اساس ID شامل کسب‌وکارها و نشست‌ها"""
repo = UserRepository(db)
user = repo.get_by_id(user_id)
if not user:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="کاربر یافت نشد")
# دریافت اطلاعات پایه کاربر
user_dict = repo.to_dict(user, include_extended=True)
# دریافت کسب‌وکارهای کاربر (هم مالک و هم عضو)
# استفاده از متد get_user_businesses که قبلاً تست شده و در business_service موجود است
from app.services.business_service import get_user_businesses
query_info = {"skip": 0, "take": 1000} # دریافت همه کسب‌وکارها بدون محدودیت
businesses_result = get_user_businesses(db, user_id, query_info)
# تبدیل فرمت کسب‌وکارها به فرمت مورد نیاز برای نمایش در مدیریت کاربران
businesses = []
for business_item in businesses_result.get("items", []):
# تبدیل نقش از فارسی به انگلیسی برای سازگاری با UI
role = business_item.get("role", "user")
if role == "مالک":
role = "owner"
elif role == "عضو":
# بررسی permissions برای تعیین نقش دقیق‌تر
permissions = business_item.get("permissions", {})
if isinstance(permissions, dict):
if permissions.get("admin"):
role = "admin"
elif permissions.get("operator"):
role = "operator"
elif permissions.get("supervisor"):
role = "supervisor"
else:
role = "user"
else:
role = "user"
businesses.append({
"id": business_item.get("id"),
"name": business_item.get("name"),
"field": business_item.get("business_field"),
"role": role,
"status": "active",
"created_at": business_item.get("created_at"),
})
user_dict["businesses"] = businesses
# دریافت نشست‌های فعال کاربر
from adapters.db.models.api_key import ApiKey
from sqlalchemy import select, desc
stmt = select(ApiKey).where(
ApiKey.user_id == user_id,
ApiKey.revoked_at.is_(None)
).order_by(desc(ApiKey.last_used_at))
sessions_list = db.execute(stmt).scalars().all()
sessions = []
for session in sessions_list:
sessions.append({
"id": session.id,
"device": session.device_id or session.user_agent or "دستگاه نامشخص",
"ip": session.ip,
"last_active_at": session.last_used_at or session.created_at,
"created_at": session.created_at,
})
user_dict["sessions"] = sessions
# دریافت آخرین فعالیت‌های کاربر
from adapters.db.repositories.activity_log_repo import ActivityLogRepository
activity_repo = ActivityLogRepository(db)
activity_logs_list = activity_repo.get_by_user(user_id, limit=50, offset=0)
audit_logs = []
for log in activity_logs_list:
audit_logs.append({
"id": log.id,
"action": log.action,
"description": log.description,
"category": log.category,
"entity_type": log.entity_type,
"entity_id": log.entity_id,
"created_at": log.created_at,
})
user_dict["audit_logs"] = audit_logs
formatted_user = format_datetime_fields(user_dict, request)
return success_response(formatted_user, request)
@router.get("/stats/summary",
summary="آمار کلی کاربران",
description="""
دریافت آمار کلی کاربران شامل تعداد کل، فعال و غیرفعال.
### اطلاعات بازگشتی:
- **total_users**: تعداد کل کاربران
- **active_users**: تعداد کاربران فعال
- **inactive_users**: تعداد کاربران غیرفعال
- **active_percentage**: درصد کاربران فعال (با 2 رقم اعشار)
### نکات:
- نیاز به سوپرادمین یا مجوز `system_settings` یا `user_management` (همان باز کردن پنل مدیریت کاربران در UI)
### مثال cURL:
```bash
curl -X GET "http://localhost:8000/api/v1/users/stats/summary" \\
-H "Authorization: Bearer sk_your_api_key"
```
""",
response_model=SuccessResponse,
responses={
200: {
"description": "آمار کاربران با موفقیت دریافت شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "آمار کاربران دریافت شد",
"data": {
"total_users": 100,
"active_users": 85,
"inactive_users": 15,
"active_percentage": 85.0
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است",
"content": {
"application/json": {
"example": {
"success": False,
"message": "Unauthorized",
"error_code": "UNAUTHORIZED"
}
}
}
},
403: {
"description": "دسترسی غیرمجاز - نیاز به مجوز user_management",
"content": {
"application/json": {
"example": {
"success": False,
"message": "Missing app permission: user_management",
"error_code": "FORBIDDEN"
}
}
}
},
429: {
"description": "تعداد درخواست‌ها بیش از حد مجاز",
"content": {
"application/json": {
"example": {
"success": False,
"message": "تعداد درخواست‌های شما بیش از حد مجاز است. لطفاً کمی صبر کنید.",
"error_code": "RATE_LIMIT_EXCEEDED"
}
}
},
"headers": {
"X-RateLimit-Limit": {"description": "حد مجاز درخواست", "schema": {"type": "integer", "example": 500}},
"X-RateLimit-Remaining": {"description": "تعداد درخواست‌های باقیمانده", "schema": {"type": "integer", "example": 0}},
"X-RateLimit-Reset": {"description": "زمان reset به ثانیه (Unix timestamp)", "schema": {"type": "integer", "example": 1705324800}},
"Retry-After": {"description": "زمان باقیمانده تا retry به ثانیه", "schema": {"type": "integer", "example": 30}}
}
}
}
)
def get_users_summary(
request: Request,
_: AuthContext = Depends(verify_user_management_page_access),
db: Session = Depends(get_db)
):
"""دریافت آمار کلی کاربران"""
repo = UserRepository(db)
# تعداد کل کاربران
total_users = repo.count_all()
# تعداد کاربران فعال
active_users = repo.query_with_filters(QueryInfo(
filters=[{"property": "is_active", "operator": "=", "value": True}]
))[1]
# تعداد کاربران غیرفعال
inactive_users = total_users - active_users
response_data = {
"total_users": total_users,
"active_users": active_users,
"inactive_users": inactive_users,
"active_percentage": round((active_users / total_users * 100), 2) if total_users > 0 else 0
}
return success_response(response_data, request)
@router.get(
"/stats/online",
summary="تعداد کاربران با فعالیت اخیر (آنلاین تقریبی)",
description="""
بر اساس فیلد `last_activity_at` (ضربان اپ کلاینت) تعداد کاربران **فعال** را می‌دهد که
در بازهٔ اخیر (پیش‌فرض ۵ دقیقه، قابل تنظیم) فعالیت ثبت کرده باشند.
تعریف «آنلاین»: کاربری که اخیراً ضربان HTTP زده؛ نه همان واقع‌زمان WebSocket مطلق.
مجوز مانند باز کردن همین صفحه در UI: سوپرادمین، `system_settings` یا `user_management`.
""",
response_model=SuccessResponse,
)
def get_users_online_stats(
request: Request,
window_minutes: int = Query(5, ge=1, le=120, description="بازه زمانی برای شمار کاربر «اخیراً فعال»"),
_: AuthContext = Depends(verify_user_management_page_access),
db: Session = Depends(get_db),
):
repo = UserRepository(db)
online_approx = repo.count_recently_active_users(within_minutes=window_minutes)
return success_response(
{
"recently_active_count": online_approx,
"window_minutes": window_minutes,
"note": "تعداد کاربران فعال با فعالیت ثبت‌شده در بازهٔ اخیر (ضربان اپ؛ تقریبی).",
},
request,
)
@router.get(
"/stats/signups-timeline",
summary="گزارش عضویت کاربران در بازهٔ تاریخی",
description="""
تعداد کاربران ثبت‌شده به‌ازای هر روز / هفته / ماه در بازهٔ مشخص (تاریخ‌ها به‌صورت UTC نیمه‌شب).
مجوز مانند `GET /users/stats/online`: سوپرادمین، `system_settings` یا `user_management`.
""",
response_model=SuccessResponse,
)
def get_users_signups_timeline(
request: Request,
start_date: date = Query(..., description="شروع بازه (شمولی)، YYYY-MM-DD"),
end_date: date = Query(..., description="پایان بازه (شمولی)، YYYY-MM-DD"),
granularity: Literal["day", "week", "month"] = Query("day", description="تجمیع روزانه، هفتگی یا ماهانه"),
_: AuthContext = Depends(verify_user_management_page_access),
db: Session = Depends(get_db),
):
if end_date < start_date:
raise ApiError("VALIDATION_ERROR", "تاریخ پایان نمی‌تواند قبل از تاریخ شروع باشد", http_status=422)
start_utc = datetime.combine(start_date, time.min)
end_exclusive = datetime.combine(end_date + timedelta(days=1), time.min)
span_days = (end_exclusive - start_utc).days
if span_days <= 0:
raise ApiError("VALIDATION_ERROR", "بازهٔ تاریخ نامعتبر است", http_status=422)
if granularity == "day" and span_days > 400:
raise ApiError("VALIDATION_ERROR", "برای تجمیع روزانه حداکثر ۴۰۰ روز مجاز است", http_status=422)
if granularity == "week" and span_days > 800:
raise ApiError("VALIDATION_ERROR", "برای تجمیع هفتگی حداکثر ۸۰۰ روز مجاز است", http_status=422)
if granularity == "month" and span_days > 3660:
raise ApiError("VALIDATION_ERROR", "برای تجمیع ماهانه حداکثر ۱۰ سال مجاز است", http_status=422)
repo = UserRepository(db)
rows = repo.get_signups_timeline_buckets(start_utc, end_exclusive, granularity)
buckets = [{"period_start": dt, "count": cnt} for dt, cnt in rows]
payload = {
"granularity": granularity,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
"buckets": buckets,
"total_in_range": sum(b["count"] for b in buckets),
}
return success_response(format_datetime_fields(payload, request), request)
@router.post("/bulk-activate",
summary="فعال‌سازی دسته‌ای کاربران",
description="""
فعال‌سازی چندین کاربر به صورت همزمان.
### نکات:
- فقط کاربران غیرفعال فعال می‌شوند
- کاربران فعال قبلاً نادیده گرفته می‌شوند
- نیاز به مجوز `user_management` در سطح اپلیکیشن دارد
- Rate Limit: 500 request در دقیقه (عمومی برای تمام endpoint ها)
- محدودیت تعداد: توصیه می‌شود حداکثر 100 کاربر در هر درخواست (بدون محدودیت سخت)
### مثال cURL:
```bash
curl -X POST "http://localhost:8000/api/v1/users/bulk-activate" \\
-H "Authorization: Bearer sk_your_api_key" \\
-H "Content-Type: application/json" \\
-d '{"user_ids": [1, 2, 3, 4, 5]}'
```
""",
response_model=SuccessResponse,
responses={
200: {
"description": "عملیات با موفقیت انجام شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "عملیات با موفقیت انجام شد",
"data": {
"updated_count": 3,
"total_requested": 5
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز - نیاز به مجوز user_management"
},
422: {
"description": "خطا در اعتبارسنجی داده‌ها",
"content": {
"application/json": {
"example": {
"success": False,
"message": "user_ids باید لیستی از اعداد باشد و حداقل 1 آیتم داشته باشد",
"error_code": "VALIDATION_ERROR"
}
}
}
},
429: {
"description": "تعداد درخواست‌ها بیش از حد مجاز",
"content": {
"application/json": {
"example": {
"success": False,
"message": "تعداد درخواست‌های شما بیش از حد مجاز است. لطفاً کمی صبر کنید.",
"error_code": "RATE_LIMIT_EXCEEDED"
}
}
},
"headers": {
"X-RateLimit-Limit": {"description": "حد مجاز درخواست", "schema": {"type": "integer", "example": 500}},
"X-RateLimit-Remaining": {"description": "تعداد درخواست‌های باقیمانده", "schema": {"type": "integer", "example": 0}},
"X-RateLimit-Reset": {"description": "زمان reset به ثانیه (Unix timestamp)", "schema": {"type": "integer", "example": 1705324800}},
"Retry-After": {"description": "زمان باقیمانده تا retry به ثانیه", "schema": {"type": "integer", "example": 30}}
}
}
}
)
@require_user_management()
def bulk_activate_users(
request: Request,
payload: BulkActivateRequest = Body(..., description="لیست شناسه‌های کاربران برای فعال‌سازی"),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""فعال‌سازی دسته‌ای کاربران"""
repo = UserRepository(db)
updated_count = 0
for user_id in payload.user_ids:
user = repo.get_by_id(user_id)
if user and not user.is_active:
user.is_active = True
updated_count += 1
db.commit()
return success_response({
"updated_count": updated_count,
"total_requested": len(payload.user_ids)
}, request)
@router.post("/bulk-suspend",
summary="تعلیق دسته‌ای کاربران",
description="""
تعلیق چندین کاربر به صورت همزمان.
### نکات:
- فقط کاربران فعال تعلیق می‌شوند
- کاربران غیرفعال قبلاً نادیده گرفته می‌شوند
- نمی‌توانید خود را تعلیق کنید (نادیده گرفته می‌شود)
- نیاز به مجوز `user_management` در سطح اپلیکیشن دارد
- Rate Limit: 500 request در دقیقه (عمومی برای تمام endpoint ها)
- محدودیت تعداد: توصیه می‌شود حداکثر 100 کاربر در هر درخواست (بدون محدودیت سخت)
### مثال cURL:
```bash
curl -X POST "http://localhost:8000/api/v1/users/bulk-suspend" \\
-H "Authorization: Bearer sk_your_api_key" \\
-H "Content-Type: application/json" \\
-d '{"user_ids": [1, 2, 3, 4, 5]}'
```
""",
response_model=SuccessResponse,
responses={
200: {
"description": "عملیات با موفقیت انجام شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "عملیات با موفقیت انجام شد",
"data": {
"updated_count": 3,
"total_requested": 5
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز - نیاز به مجوز user_management"
},
422: {
"description": "خطا در اعتبارسنجی داده‌ها",
"content": {
"application/json": {
"example": {
"success": False,
"message": "user_ids باید لیستی از اعداد باشد و حداقل 1 آیتم داشته باشد",
"error_code": "VALIDATION_ERROR"
}
}
}
},
429: {
"description": "تعداد درخواست‌ها بیش از حد مجاز",
"content": {
"application/json": {
"example": {
"success": False,
"message": "تعداد درخواست‌های شما بیش از حد مجاز است. لطفاً کمی صبر کنید.",
"error_code": "RATE_LIMIT_EXCEEDED"
}
}
},
"headers": {
"X-RateLimit-Limit": {"description": "حد مجاز درخواست", "schema": {"type": "integer", "example": 500}},
"X-RateLimit-Remaining": {"description": "تعداد درخواست‌های باقیمانده", "schema": {"type": "integer", "example": 0}},
"X-RateLimit-Reset": {"description": "زمان reset به ثانیه (Unix timestamp)", "schema": {"type": "integer", "example": 1705324800}},
"Retry-After": {"description": "زمان باقیمانده تا retry به ثانیه", "schema": {"type": "integer", "example": 30}}
}
}
}
)
@require_user_management()
def bulk_suspend_users(
request: Request,
payload: BulkSuspendRequest = Body(..., description="لیست شناسه‌های کاربران برای تعلیق"),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""تعلیق دسته‌ای کاربران"""
repo = UserRepository(db)
updated_count = 0
for user_id in payload.user_ids:
user = repo.get_by_id(user_id)
if user and user.is_active:
# جلوگیری از تعلیق خود کاربر
if user.id == ctx.get_user_id():
continue
user.is_active = False
updated_count += 1
db.commit()
return success_response({
"updated_count": updated_count,
"total_requested": len(payload.user_ids)
}, request)
@router.post("/bulk-reset-password",
summary="بازنشانی رمز عبور دسته‌ای کاربران",
description="""
ایجاد توکن بازنشانی رمز عبور برای چندین کاربر.
### نکات:
- برای هر کاربر یک توکن منحصر به فرد ایجاد می‌شود
- فقط برای کاربرانی که ایمیل یا موبایل دارند توکن ایجاد می‌شود
- توکن‌ها به صورت خودکار منقضی می‌شوند (بر اساس تنظیمات سیستم)
- نیاز به مجوز `user_management` در سطح اپلیکیشن دارد
- Rate Limit: 500 request در دقیقه (عمومی برای تمام endpoint ها)
- محدودیت تعداد: توصیه می‌شود حداکثر 100 کاربر در هر درخواست (بدون محدودیت سخت)
### مثال cURL:
```bash
curl -X POST "http://localhost:8000/api/v1/users/bulk-reset-password" \\
-H "Authorization: Bearer sk_your_api_key" \\
-H "Content-Type: application/json" \\
-d '{"user_ids": [1, 2, 3, 4, 5]}'
```
""",
response_model=SuccessResponse,
responses={
200: {
"description": "توکن‌ها با موفقیت ایجاد شدند",
"content": {
"application/json": {
"example": {
"success": True,
"message": "توکن‌ها با موفقیت ایجاد شدند",
"data": {
"tokens_created": 4,
"total_requested": 5
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز - نیاز به مجوز user_management"
},
422: {
"description": "خطا در اعتبارسنجی داده‌ها",
"content": {
"application/json": {
"example": {
"success": False,
"message": "user_ids باید لیستی از اعداد باشد و حداقل 1 آیتم داشته باشد",
"error_code": "VALIDATION_ERROR"
}
}
}
},
429: {
"description": "تعداد درخواست‌ها بیش از حد مجاز",
"content": {
"application/json": {
"example": {
"success": False,
"message": "تعداد درخواست‌های شما بیش از حد مجاز است. لطفاً کمی صبر کنید.",
"error_code": "RATE_LIMIT_EXCEEDED"
}
}
},
"headers": {
"X-RateLimit-Limit": {"description": "حد مجاز درخواست", "schema": {"type": "integer", "example": 500}},
"X-RateLimit-Remaining": {"description": "تعداد درخواست‌های باقیمانده", "schema": {"type": "integer", "example": 0}},
"X-RateLimit-Reset": {"description": "زمان reset به ثانیه (Unix timestamp)", "schema": {"type": "integer", "example": 1705324800}},
"Retry-After": {"description": "زمان باقیمانده تا retry به ثانیه", "schema": {"type": "integer", "example": 30}}
}
}
}
)
@require_user_management()
def bulk_reset_password(
request: Request,
payload: BulkResetPasswordRequest = Body(..., description="لیست شناسه‌های کاربران برای بازنشانی رمز عبور"),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""بازنشانی رمز عبور دسته‌ای کاربران"""
from app.services.auth_service import _hash_reset_token
from adapters.db.repositories.password_reset_repo import PasswordResetRepository
from app.core.settings import get_settings
from secrets import token_urlsafe
from datetime import datetime, timedelta
repo = UserRepository(db)
tokens_created = 0
settings = get_settings()
pr_repo = PasswordResetRepository(db)
for user_id in payload.user_ids:
user = repo.get_by_id(user_id)
if user:
identifier = user.email or user.mobile
if identifier:
try:
token = token_urlsafe(32)
token_hash = _hash_reset_token(token)
expires_at = datetime.utcnow() + timedelta(seconds=settings.reset_password_ttl_seconds)
pr_repo.create(user_id=user.id, token_hash=token_hash, expires_at=expires_at)
tokens_created += 1
if payload.send_notification:
rlink = build_password_reset_web_url(request=request, token=token)
send_password_reset_notification(
db=db, user_id=user.id, token=token, reset_link=rlink
)
except Exception:
pass # در صورت خطا ادامه می‌دهیم
db.commit()
return success_response({
"tokens_created": tokens_created,
"total_requested": len(payload.user_ids)
}, request)
@router.post("/{user_id}/suspend",
summary="تعلیق یک کاربر",
description="""
تعلیق یک کاربر خاص.
### نکات:
- نمی‌توانید خود را تعلیق کنید
- نیاز به مجوز `user_management` در سطح اپلیکیشن دارد
- Rate Limit: 500 request در دقیقه (عمومی برای تمام endpoint ها)
### مثال cURL:
```bash
curl -X POST "http://localhost:8000/api/v1/users/123/suspend" \\
-H "Authorization: Bearer sk_your_api_key"
```
""",
response_model=SuccessResponse,
responses={
200: {
"description": "کاربر با موفقیت تعلیق شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "کاربر با موفقیت تعلیق شد",
"data": {
"message": "کاربر با موفقیت تعلیق شد"
}
}
}
}
},
400: {
"description": "نمی‌توانید خود را تعلیق کنید",
"content": {
"application/json": {
"example": {
"success": False,
"message": "نمی‌توانید خود را تعلیق کنید",
"error_code": "CANNOT_SUSPEND_SELF"
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز - نیاز به مجوز user_management"
},
404: {
"description": "کاربر یافت نشد",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کاربر یافت نشد",
"error_code": "USER_NOT_FOUND"
}
}
}
},
429: {
"description": "تعداد درخواست‌ها بیش از حد مجاز",
"content": {
"application/json": {
"example": {
"success": False,
"message": "تعداد درخواست‌های شما بیش از حد مجاز است. لطفاً کمی صبر کنید.",
"error_code": "RATE_LIMIT_EXCEEDED"
}
}
},
"headers": {
"X-RateLimit-Limit": {"description": "حد مجاز درخواست", "schema": {"type": "integer", "example": 500}},
"X-RateLimit-Remaining": {"description": "تعداد درخواست‌های باقیمانده", "schema": {"type": "integer", "example": 0}},
"X-RateLimit-Reset": {"description": "زمان reset به ثانیه (Unix timestamp)", "schema": {"type": "integer", "example": 1705324800}},
"Retry-After": {"description": "زمان باقیمانده تا retry به ثانیه", "schema": {"type": "integer", "example": 30}}
}
}
}
)
@require_user_management()
def suspend_user(
user_id: int,
request: Request,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""تعلیق یک کاربر"""
repo = UserRepository(db)
user = repo.get_by_id(user_id)
if not user:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="کاربر یافت نشد")
# جلوگیری از تعلیق خود کاربر
if user.id == ctx.get_user_id():
from app.core.responses import ApiError
raise ApiError("CANNOT_SUSPEND_SELF", "نمی‌توانید خود را تعلیق کنید", http_status=400)
user.is_active = False
db.commit()
return success_response({"message": "کاربر با موفقیت تعلیق شد"}, request)
@router.post("/{user_id}/activate",
summary="فعال‌سازی یک کاربر",
description="""
فعال‌سازی یک کاربر خاص.
### نکات:
- نیاز به مجوز `user_management` در سطح اپلیکیشن دارد
- Rate Limit: 500 request در دقیقه (عمومی برای تمام endpoint ها)
### مثال cURL:
```bash
curl -X POST "http://localhost:8000/api/v1/users/123/activate" \\
-H "Authorization: Bearer sk_your_api_key"
```
""",
response_model=SuccessResponse,
responses={
200: {
"description": "کاربر با موفقیت فعال شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "کاربر با موفقیت فعال شد",
"data": {
"message": "کاربر با موفقیت فعال شد"
}
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز - نیاز به مجوز user_management"
},
404: {
"description": "کاربر یافت نشد",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کاربر یافت نشد",
"error_code": "USER_NOT_FOUND"
}
}
}
},
429: {
"description": "تعداد درخواست‌ها بیش از حد مجاز",
"content": {
"application/json": {
"example": {
"success": False,
"message": "تعداد درخواست‌های شما بیش از حد مجاز است. لطفاً کمی صبر کنید.",
"error_code": "RATE_LIMIT_EXCEEDED"
}
}
},
"headers": {
"X-RateLimit-Limit": {"description": "حد مجاز درخواست", "schema": {"type": "integer", "example": 500}},
"X-RateLimit-Remaining": {"description": "تعداد درخواست‌های باقیمانده", "schema": {"type": "integer", "example": 0}},
"X-RateLimit-Reset": {"description": "زمان reset به ثانیه (Unix timestamp)", "schema": {"type": "integer", "example": 1705324800}},
"Retry-After": {"description": "زمان باقیمانده تا retry به ثانیه", "schema": {"type": "integer", "example": 30}}
}
}
}
)
@require_user_management()
def activate_user(
user_id: int,
request: Request,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""فعال‌سازی یک کاربر"""
repo = UserRepository(db)
user = repo.get_by_id(user_id)
if not user:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="کاربر یافت نشد")
user.is_active = True
db.commit()
return success_response({"message": "کاربر با موفقیت فعال شد"}, request)
@router.post("/{user_id}/reset-password",
summary="بازنشانی رمز عبور یک کاربر",
description="""
ایجاد توکن بازنشانی رمز عبور برای یک کاربر.
### نکات:
- توکن به صورت خودکار منقضی می‌شود (بر اساس تنظیمات سیستم)
- کاربر باید ایمیل یا موبایل داشته باشد
- اگر `send_notification=true` باشد (پیش‌فرض)، اعلان `auth.password_reset` برای کاربر ارسال می‌شود
- فیلد `token` فقط وقتی در پاسخ می‌آید که `app.debug` در تنظیمات API روشن باشد
- نیاز به مجوز `user_management` در سطح اپلیکیشن دارد
- Rate Limit: 500 request در دقیقه (عمومی برای تمام endpoint ها)
### مثال cURL:
```bash
curl -X POST "http://localhost:8000/api/v1/users/123/reset-password?send_notification=true" \\
-H "Authorization: Bearer sk_your_api_key"
```
""",
response_model=SuccessResponse,
responses={
200: {
"description": "توکن بازنشانی رمز عبور ایجاد شد",
"content": {
"application/json": {
"example": {
"success": True,
"message": "توکن بازنشانی رمز عبور ایجاد شد",
"data": {
"message": "توکن بازنشانی رمز عبور ایجاد شد",
"token": "reset_token_1234567890abcdef"
}
}
}
}
},
400: {
"description": "کاربر ایمیل یا موبایل ندارد",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کاربر ایمیل یا موبایل ندارد",
"error_code": "NO_IDENTIFIER"
}
}
}
},
401: {
"description": "کاربر احراز هویت نشده است"
},
403: {
"description": "دسترسی غیرمجاز - نیاز به مجوز user_management"
},
404: {
"description": "کاربر یافت نشد",
"content": {
"application/json": {
"example": {
"success": False,
"message": "کاربر یافت نشد",
"error_code": "USER_NOT_FOUND"
}
}
}
},
429: {
"description": "تعداد درخواست‌ها بیش از حد مجاز",
"content": {
"application/json": {
"example": {
"success": False,
"message": "تعداد درخواست‌های شما بیش از حد مجاز است. لطفاً کمی صبر کنید.",
"error_code": "RATE_LIMIT_EXCEEDED"
}
}
},
"headers": {
"X-RateLimit-Limit": {"description": "حد مجاز درخواست", "schema": {"type": "integer", "example": 500}},
"X-RateLimit-Remaining": {"description": "تعداد درخواست‌های باقیمانده", "schema": {"type": "integer", "example": 0}},
"X-RateLimit-Reset": {"description": "زمان reset به ثانیه (Unix timestamp)", "schema": {"type": "integer", "example": 1705324800}},
"Retry-After": {"description": "زمان باقیمانده تا retry به ثانیه", "schema": {"type": "integer", "example": 30}}
}
}
}
)
@require_user_management()
def reset_user_password(
user_id: int,
request: Request,
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
send_notification: bool = Query(
default=True,
description="ارسال اعلان بازیابی رمز (همان الگوی فراموشی رمز)",
),
):
"""بازنشانی رمز عبور یک کاربر (توکن + اختیار اعلان)"""
from adapters.db.repositories.password_reset_repo import PasswordResetRepository
from secrets import token_urlsafe
from datetime import datetime, timedelta
repo = UserRepository(db)
user = repo.get_by_id(user_id)
if not user:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="کاربر یافت نشد")
identifier = user.email or user.mobile
if not identifier:
from app.core.responses import ApiError
raise ApiError("NO_IDENTIFIER", "کاربر ایمیل یا موبایل ندارد", http_status=400)
settings = get_settings()
pr_repo = PasswordResetRepository(db)
token = token_urlsafe(32)
token_hash = _hash_reset_token(token)
expires_at = datetime.utcnow() + timedelta(seconds=settings.reset_password_ttl_seconds)
pr_repo.create(user_id=user.id, token_hash=token_hash, expires_at=expires_at)
reset_link = build_password_reset_web_url(request=request, token=token)
if send_notification:
send_password_reset_notification(
db=db, user_id=user.id, token=token, reset_link=reset_link
)
data: dict = {
"message": "توکن بازنشانی رمز عبور ایجاد شد",
"send_notification": send_notification,
"reset_link": reset_link,
}
if settings.debug:
data["token"] = token
return success_response(data, request)
@router.post(
"/{user_id}/set-password",
summary="تنظیم رمز توسط مدیر (انتخابی یا تصادفی)",
description="""
تنظیم مستقیم `password_hash` برای کاربر بدون توکن بازنشانی.
- `mode=direct`: `new_password` و `confirm_password` (۸ تا ۱۲۸ نویسه)
- `mode=random`: رمز امن تولید می‌شود و **فقط یک‌بار** در `plain_password` برمی‌گردد
نیاز به مجوز `user_management`.
""",
response_model=SuccessResponse,
)
@require_user_management()
def admin_set_user_password_endpoint(
user_id: int,
request: Request,
payload: AdminSetUserPasswordRequest = Body(..., description="حالت و رمز"),
ctx: AuthContext = Depends(get_current_user),
db: Session = Depends(get_db),
):
result = admin_set_user_password(
db=db,
target_user_id=user_id,
mode=payload.mode,
new_password=payload.new_password,
confirm_password=payload.confirm_password,
)
out: dict = {
"message": "رمز عبور به‌روزرسانی شد",
"mode": payload.mode,
}
plain = result.get("plain_password")
if plain:
out["plain_password"] = plain
return success_response(out, request)