File size: 28,646 Bytes
3e7b272
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e7b272
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0342716
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e7b272
a6fd1a3
 
 
 
 
 
 
 
 
 
 
5137439
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0342716
a6fd1a3
 
 
 
 
6b9df75
a6fd1a3
 
5137439
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e7b272
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
3e7b272
a6fd1a3
 
 
 
 
 
 
 
 
 
5137439
a6fd1a3
 
 
 
 
 
 
 
3e7b272
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
8d71681
a6fd1a3
 
 
 
 
 
 
 
 
 
 
5137439
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e7b272
a6fd1a3
 
 
 
 
5137439
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5137439
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e7b272
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e7b272
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5137439
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d71681
a6fd1a3
 
 
3e7b272
 
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
5137439
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530a737
a6fd1a3
 
 
 
 
 
 
 
 
 
530a737
a6fd1a3
 
 
 
 
5137439
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d71681
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d71681
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5137439
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530a737
a6fd1a3
 
 
 
 
 
 
 
 
 
530a737
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e7b272
a6fd1a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
from db.mongoDB import mongo_db
from fastapi import HTTPException, Request, Response
import math
from datetime import datetime, timedelta, timezone
from fastapi import status
import logging
import secrets
from bson import ObjectId
from passlib.context import CryptContext
from services.send_email import send_reset_password_email,send_verification_code_email,send_suspicious_activity_email
from schemas.user import LoginRequest
from utils.utils import create_access_token,create_refresh_token
import os


logger = logging.getLogger(__name__)
# Initialize password hashing context
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

async def get_users(skip: int = 0, limit: int = 100, query: dict = None):
    """
    Lấy danh sách người dùng với phân trang và tìm kiếm nâng cao

    Args:
        skip (int): Số lượng bản ghi bỏ qua (để phân trang)
        limit (int): Số lượng bản ghi tối đa trả về
        query (dict, optional): Điều kiện tìm kiếm người dùng

    Returns:
        list: Danh sách người dùng đã được lọc và phân trang

    Raises:
        HTTPException: Nếu có lỗi xảy ra khi truy vấn cơ sở dữ liệu
    """
    try:
        # Khởi tạo query nếu chưa có
        search_query = query or {}

        # Thực hiện truy vấn với phân trang
        users_cursor =  mongo_db.users.find(
            search_query,
            {"_id": 0, "password": 0}  # Loại bỏ các trường nhạy cảm
        ).skip(skip).limit(limit)

        # Chuyển cursor thành danh sách
        user_list = list(users_cursor)

        # Thêm log để debug nếu cần
        logger.debug(f"Đã lấy {len(user_list)} người dùng với skip={skip}, limit={limit}")

        return user_list
    except Exception as e:
        logger.error(f"Lỗi khi lấy danh sách người dùng: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Lỗi khi lấy danh sách người dùng: {str(e)}"
        )

async def count_users(query: dict = None):
    """
    Đếm tổng số người dùng thỏa mãn điều kiện tìm kiếm

    Args:
        query (dict, optional): Điều kiện tìm kiếm người dùng

    Returns:
        int: Tổng số người dùng
    """
    try:
        search_query = query or {}
        return await mongo_db.users.count_documents(search_query)
    except Exception as e:
        logger.error(f"Lỗi khi đếm người dùng: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Lỗi khi đếm người dùng: {str(e)}"
        )

async def get_paginated_users(
    skip: int = 0,
    limit: int = 10,
    search: str = None,
    sort_by: str = "created_at",
    sort_order: int = -1
):
    """
    Lấy danh sách người dùng với đầy đủ thông tin phân trang và tìm kiếm

    Args:
        skip (int): Số lượng bản ghi bỏ qua
        limit (int): Số lượng bản ghi tối đa trả về
        search (str, optional): Từ khóa tìm kiếm (tên, email, v.v.)
        sort_by (str): Trường để sắp xếp
        sort_order (int): Thứ tự sắp xếp (1: tăng dần, -1: giảm dần)

    Returns:
        dict: Kết quả phân trang với items và metadata
    """
    try:
        # Xây dựng query tìm kiếm nếu có từ khóa
        query = {}
        if search:
            # Tìm kiếm theo tên hoặc email
            query = {
                "$or": [
                    {"email": {"$regex": search, "$options": "i"}},
                    {"username": {"$regex": search, "$options": "i"}}
                ]
            }

        # Sắp xếp
        sort_criteria = [(sort_by, sort_order)]

        # Thực hiện truy vấn
        users_cursor =  mongo_db.users.find(
            query,
            {"_id": 0, "password": 0}  # Loại bỏ các trường nhạy cảm
        ).sort(sort_criteria).skip(skip).limit(limit)

        # Chuyển cursor thành danh sách
        user_list = await users_cursor.to_list(length=limit)

        # Đếm tổng số bản ghi
        total =await mongo_db.users.count_documents(query)

        # Tính toán thông tin phân trang
        total_pages = math.ceil(total / limit) if limit > 0 else 0
        current_page = math.floor(skip / limit) + 1 if limit > 0 else 1

        # Trả về kết quả với metadata phân trang
        return {
            "items": user_list,
            "metadata": {
                "total": total,
                "page": current_page,
                "page_size": limit,
                "pages": total_pages,
                "has_more": current_page < total_pages
            }
        }
    except Exception as e:
        logger.error(f"Lỗi khi lấy danh sách người dùng phân trang: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Lỗi khi lấy danh sách người dùng: {str(e)}"
        )

async def get_current_user_profile(email: str):
    """
    Lấy thông tin profile của người dùng hiện tại

    Args:
        email (str): Email của người dùng

    Returns:
        dict: Thông tin profile của người dùng
    """
    try:
        user =await  mongo_db.users.find_one({"email": email}, {"_id": 0, "password": 0})
        logger.info(f"check user user_service: {user}")
        if not user:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Người dùng không tồn tại")
        return user
    except HTTPException as he:
        raise he
    except Exception as e:
        logger.error(f"Lỗi khi lấy thông tin profile người dùng: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Lỗi khi lấy thông tin profile người dùng: {str(e)}"
        )


async def delete_user(user_id: str):
    """
    Xóa người dùng theo ID

    Args:
        user_id (str): ID của người dùng cần xóa

    Returns:
        bool: True nếu xóa thành công, False nếu không tìm thấy người dùng
    """
    try:
        result =await mongo_db.users.delete_one({"_id": user_id})
        return result.deleted_count > 0
    except Exception as e:
        logger.error(f"Lỗi khi xóa người dùng: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Lỗi khi xóa người dùng: {str(e)}"
        )

async def change_password(email: str, current_password: str, new_password: str):
    """
    Đổi mật khẩu của người dùng

    Args:
        user_id (str): ID của người dùng
        current_password (str): Mật khẩu hiện tại
        new_password (str): Mật khẩu mới

    Returns:
        bool: True nếu đổi mật khẩu thành công, False nếu không thành công
    """
    try:
        user =await mongo_db.users.find_one({"email": email})
        if not user:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Người dùng không tồn tại")



        if not pwd_context.verify(current_password, user["password"]):
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Mật khẩu hiện tại không chính xác")

        # Hash the new password
        hashed_password = pwd_context.hash(new_password)
        # Update the password in the database
        result =await mongo_db.users.update_one({"email": email}, {"$set": {"password": hashed_password}})
        return result.modified_count > 0
    except HTTPException as he:
        raise he
    except Exception as e:
        logger.error(f"Lỗi khi đổi mật khẩu: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Lỗi khi đổi mật khẩu"
        )

async def reset_password_request(email: str) -> bool:
    """
    Yêu cầu đặt lại mật khẩu cho người dùng

    Args:
        email (str): Địa chỉ email của người dùng

    Returns:
        bool: True nếu yêu cầu thành công, False nếu không tìm thấy người dùng

    Raises:
        HTTPException: Nếu có lỗi trong quá trình xử lý
    """
    try:
        # Check if user exists
        user = await  mongo_db.users.find_one({"email": email.lower()})
        if not user:
            # Return True even if user not found to prevent email enumeration
            return True

        # Check for rate limiting (e.g., max 3 requests per hour)
        reset_requests =await mongo_db.users.count_documents({
            "email": email.lower(),
            "reset_password_timestamp": {
                "$gte": datetime.now(tz=timezone.utc) - timedelta(hours=1)
            }
        })
        if reset_requests >= 3:
            raise HTTPException(
                status_code=status.HTTP_429_TOO_MANY_REQUESTS,
                detail="Quá nhiều yêu cầu đặt lại mật khẩu. Vui lòng thử lại sau."
            )

        # Generate secure reset token
        code = str(secrets.randbelow(1000000)).zfill(6)  # e.g., '123456'
        expiry = datetime.now() + timedelta(minutes=10)

        # Store reset token and timestamp in database
        await mongo_db.users.update_one(
            {"_id": ObjectId(user["_id"])},
            {
                "$set": {
                    "reset_password_code": code,
                    "reset_password_timestamp": datetime.now(tz=timezone.utc),
                    "reset_password_expiry": expiry,
                }
            }
        )

        # Send reset password email (uncomment and implement actual email sending)
        await send_reset_password_email(
            email=email,
            code=code,
            expiry=15
        )

        return True

    except HTTPException as he:
        raise he
    except Exception as e:
        logger.error(f"Lỗi khi yêu cầu đặt lại mật khẩu: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Lỗi hệ thống. Vui lòng thử lại sau."
        )



async def reset_password(code: str, newPassword: str) -> bool:
    """
    Hoàn tất đặt lại mật khẩu với token và mật khẩu mới.

    Args:
        code (str): code đặt lại mật khẩu.
        newPassword (str): Mật khẩu mới của người dùng.

    Returns:
        bool: True nếu đặt lại mật khẩu thành công, False nếu token không hợp lệ hoặc đã hết hạn.

    Raises:
        HTTPException: Nếu có lỗi hệ thống trong quá trình xử lý.
    """
    try:
        # Find user by reset token
        user =await  mongo_db.users.find_one({"reset_password_code": code})
        if not user:
            logger.warning(f"Code không hợp lệ: {code}")
            return False

        # Check if token is expired
        expiry = user.get("reset_password_expiry")
        if not expiry or expiry < datetime.now():
            logger.warning(f"Code đã hết hạn: {code}")
            return False

        # Validate new password (basic example, can add more rules)
        if len(newPassword) < 8:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Mật khẩu mới phải có ít nhất 8 ký tự"
            )

        # Hash the new password
        hashed_password = pwd_context.hash(newPassword)

        # Update user's password and clear reset token
        result =await  mongo_db.users.update_one(
            {"_id": ObjectId(user["_id"])},
            {
                "$set": {"password": hashed_password},
                "$unset": {
                    "reset_password_code": "",
                    "reset_password_expiry": "",
                    "reset_password_timestamp": ""
                }
            }
        )

        if result.modified_count != 1:
            logger.error(f"Không thể cập nhật mật khẩu cho user: {user['_id']}")
            return False

        logger.info(f"Đặt lại mật khẩu thành công cho email: {user['email']}")
        return True

    except Exception as e:
        logger.error(f"Lỗi khi đặt lại mật khẩu: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Lỗi hệ thống khi đặt lại mật khẩu. Vui lòng thử lại sau."
        )


async def generate_and_store_verification_code(email: str) -> bool:
    """
    Tạo và lưu mã xác minh đăng nhập vào cơ sở dữ liệu.

    Args:
        email (str): Địa chỉ email của người dùng.

    Returns:
        bool: True nếu lưu mã thành công.

    Raises:
        HTTPException: Nếu có lỗi hệ thống.
    """
    try:
        # Generate 6-digit code
        code = str(secrets.randbelow(1000000)).zfill(6)  # e.g., '123456'
        expiry = datetime.now() + timedelta(minutes=10)

        # Store code and expiry in database
        result =await mongo_db.users.update_one(
            {"email": email.lower()},
            {
                "$set": {
                    "login_verification_code": code,
                    "login_verification_expiry": expiry,
                    "login_verification_timestamp": datetime.now()
                }
            }
        )

        if result.modified_count != 1:
            logger.error(f"Không thể lưu mã xác minh cho email: {email}")
            return False

        # Send verification email
        await send_verification_code_email(email, code)
        return True

    except Exception as e:
        logger.error(f"Lỗi khi tạo mã xác minh: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Lỗi hệ thống khi tạo mã xác minh. Vui lòng thử lại sau."
        )



async def verify_login_code(email: str, code: str, res: Response): # Bỏ kiểu trả về dict, để FastAPI tự suy luận hoặc dùng Pydantic model
    """
    Xác minh mã đăng nhập, set access và refresh tokens vào HttpOnly cookies,
    và trả về thông tin người dùng.

    Args:
        email (str): Địa chỉ email của người dùng.
        code (str): Mã xác minh do người dùng cung cấp.
        res (Response): Đối tượng Response của FastAPI để set cookies.

    Returns:
        dict: Thông tin người dùng và thông báo thành công.

    Raises:
        HTTPException: Nếu mã không hợp lệ, đã hết hạn hoặc có lỗi hệ thống.
    """
    try:
        user = await mongo_db.users.find_one({ # Sử dụng await nếu mongo_db.users là async (ví dụ Motor)
            "email": email.lower(),
            "login_verification_code": code
        })
        # Nếu mongo_db.users là đồng bộ (ví dụ PyMongo)
        # user = mongo_db.users.find_one({ ... })

        if not user:
            logger.warning(f"Mã xác minh không hợp lệ: {code} cho email: {email}")
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Mã xác minh không hợp lệ hoặc đã được sử dụng." # Thêm "đã được sử dụng"
            )

        expiry = user.get("login_verification_expiry")
        if not expiry or expiry < datetime.now(expiry.tzinfo if expiry.tzinfo else None): # So sánh aware với aware, naive với naive
            logger.warning(f"Mã xác minh đã hết hạn cho email: {email}")
            # Xóa mã đã hết hạn để tránh sử dụng lại
            await mongo_db.users.update_one(
                {"_id": ObjectId(user["_id"])},
                {
                    "$unset": {
                        "login_verification_code": "",
                        "login_verification_expiry": "",
                        "login_verification_timestamp": ""
                    }
                }
            )
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Mã xác minh đã hết hạn."
            )

        try:
            token_expire_minutes_str = os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60") # Default là chuỗi "60"
            ACCESS_TOKEN_EXPIRE_MINUTES_INT = int(token_expire_minutes_str)
            if ACCESS_TOKEN_EXPIRE_MINUTES_INT <= 0:
                logger.warning(f"Giá trị ACCESS_TOKEN_EXPIRE_MINUTES ('{token_expire_minutes_str}') không hợp lệ, sử dụng mặc định 60 phút.")
                ACCESS_TOKEN_EXPIRE_MINUTES_INT = 60
        except ValueError:
            logger.error(f"Không thể chuyển đổi ACCESS_TOKEN_EXPIRE_MINUTES ('{os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES')}') sang số nguyên. Sử dụng mặc định 60 phút.")
            ACCESS_TOKEN_EXPIRE_MINUTES_INT = 60

        logger.info(f"Sử dụng thời gian hết hạn cho access token: {ACCESS_TOKEN_EXPIRE_MINUTES_INT} phút.")
        access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES_INT) # Sử dụng biến đã chuyển đổi
        refresh_token_expires = timedelta(days=7) # Sử dụng biến đã chuyển đổi
        access_token = create_access_token(
            data={"sub": email.lower()}, expires_delta=access_token_expires
        ) # Giả sử create_access_token chấp nhận expires_delta

        IS_PRODUCTION = os.getenv("APP_ENVIRONMENT", "development").lower() == "production"
        # Generate refresh token (logic của bạn đã có)
        refresh_token_value = await  create_refresh_token(email.lower()) # Đảm bảo email là lowercase
        # --- SET COOKIES ---
        # Môi trường (ví dụ: "development", "production")
        # Bạn nên có một biến môi trường để xác định điều này
        # IS_PRODUCTION = os.getenv("ENVIRONMENT") == "production"
        # Tạm đặt là True, bạn nên lấy từ biến môi trường
        max_age_seconds = int(access_token_expires.total_seconds())
        logger.info(f"Setting access_token_cookie with Max-Age: {max_age_seconds} seconds")
        # Access Token Cookie
        res.set_cookie(
            key="access_token_cookie", # Tên cookie cho access token
            value=access_token,
            max_age=int(access_token_expires.total_seconds()), # Thời gian sống bằng access token
            httponly=True,
            secure=IS_PRODUCTION, # True trong production (HTTPS), False khi dev với HTTP
            samesite="none", # Hoặc "strict"
            path="/",
        )

        # Refresh Token Cookie (logic của bạn đã có, điều chỉnh secure)
        res.set_cookie(
            key="refresh_token",
            value=refresh_token_value,
            max_age=int(refresh_token_expires.total_seconds()),
            httponly=True,
            secure=IS_PRODUCTION, # True trong production, False khi dev với HTTP
            samesite="none",
            path="/api/user/refresh-token",
        )
        # --------------------

        # Clear verification code và cập nhật last_login
        await mongo_db.users.update_one( # Sử dụng await nếu là async
            {"_id": ObjectId(user["_id"])},
            {
                "$unset": {
                    "login_verification_code": "",
                    "login_verification_expiry": "",
                    "login_verification_timestamp": ""
                },
                "$set": {
                    "last_login": datetime.now(timezone.utc if expiry.tzinfo else None), # Giữ timezone nhất quán
                    "login_type": "email", # Giả sử đây là đăng nhập bằng email
                }
            }
        )
        # if update_result.modified_count == 0:
            # logger.warning(f"Không thể cập nhật user sau khi xác minh: {email}")
            # Cân nhắc có nên raise lỗi ở đây không, hoặc chỉ log

        logger.info(f"Đăng nhập thành công cho email: {email}. Tokens đã được set vào cookies.")

        # Response trả về không còn chứa accessToken
        return {
            # "accessToken": access_token, # KHÔNG TRẢ VỀ ACCESS TOKEN TRONG BODY NỮA
            "user": {
                "email": email.lower(),
                "username": user.get("username", email.lower().split('@')[0]), # Cung cấp username mặc định nếu không có
                "role": user.get("role", "user"),
                "avatar_url": user.get("avatar_url", None), # Hoặc một avatar mặc định,
                "login_type": user.get("login_type", "email") # Giả sử đây là đăng nhập bằng email
            },
            "message": "Đăng nhập thành công. Tokens đã được lưu trữ an toàn."
        }

    except HTTPException as he:
        # Ghi log chi tiết hơn cho HTTPException nếu cần
        # logger.error(f"HTTPException trong verify_login_code cho {email}: {he.detail}", exc_info=not (he.status_code < 500))
        raise he
    except Exception as e:
        logger.error(f"Lỗi hệ thống khi xác minh mã đăng nhập cho {email}: {e}", exc_info=True)
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Lỗi hệ thống, vui lòng thử lại sau."
        )


async def authenticate_user(request: LoginRequest) -> dict:
    """
    Xác thực người dùng dựa trên email và mật khẩu.

    Args:
        request (LoginRequest): Yêu cầu đăng nhập chứa email và mật khẩu.

    Returns:
        dict: Thông tin người dùng nếu xác thực thành công.

    Raises:
        HTTPException: Nếu thông tin đăng nhập không hợp lệ.
    """
    user = await mongo_db.users.find_one({"email": request.email.lower()})
    if not user or not pwd_context.verify(request.password, user["password"]):
        logger.warning(f"Xác thực thất bại cho email: {request.email}")
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Email hoặc mật khẩu không đúng",
            headers={"WWW-Authenticate": "Bearer"}
        )
    return user


async def refresh_access_token(req: Request  , res: Response ) -> dict:
    """
    Làm mới access token dựa trên refresh token từ cookie.

    Args:
        req (Request): FastAPI request object chứa cookie.

    Returns:
        JSONResponse: Phản hồi chứa access_token, refresh_token, username, email, role và set cookie mới.

    Raises:
        HTTPException: Nếu refresh token không hợp lệ, đã hết hạn hoặc có lỗi hệ thống.
    """
    try:
        # Retrieve refresh token from cookie
        refresh_token = req.cookies.get("refresh_token")
        if not refresh_token:
            logger.warning("Không tìm thấy refresh token trong cookie")
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Không tìm thấy refresh token",
                headers={"WWW-Authenticate": "Bearer"}
            )

        # Find user by refresh token in database
        user =await mongo_db.users.find_one({"refresh_token": refresh_token})


        if not user:
            logger.warning(f"Refresh token không hợp lệ: {refresh_token}")
            await send_suspicious_activity_email(user.get("email", "unknown") if user else "unknown")
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Refresh token không hợp lệ",
                headers={"WWW-Authenticate": "Bearer"}
            )

        # Check if refresh token is expired
        expiry = user.get("refresh_token_expiry")
        if not expiry or expiry < datetime.now():
            logger.warning(f"Refresh token đã hết hạn cho email: {user['email']}")
            await send_suspicious_activity_email(user["email"])
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Refresh token đã hết hạn",
                headers={"WWW-Authenticate": "Bearer"}
            )

        email = user.get('email')
        # Generate new access token
        token_expire_minutes_str = os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60") # Default là chuỗi "60"
        ACCESS_TOKEN_EXPIRE_MINUTES_INT = int(token_expire_minutes_str)
        access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES_INT) # Sử dụng biến đã chuyển đổi
        access_token = create_access_token(
            data={"sub": email.lower()}, expires_delta=access_token_expires
        ) # Giả sử create_access_token chấp nhận expires_delta

        IS_PRODUCTION = os.getenv("APP_ENVIRONMENT", "development").lower() == "production"
        # Generate new refresh token (rotation)
        new_refresh_token = await create_refresh_token(email)

        # Update database with new refresh token
        await mongo_db.users.update_one(
            {"_id": user["_id"]},
            {
                "$set": {
                    "refresh_token": new_refresh_token,
                    "refresh_token_expiry": datetime.now() + timedelta(days=7),
                    "last_login": datetime.now(timezone.utc)
                }
            }
        )



         # Set access token mới vào cookie
        res.set_cookie(
            key="access_token_cookie", # Đảm bảo tên này khớp với client mong đợi
            value=access_token,
            max_age= access_token_expires, # tính bằng giây
            httponly=True,
            secure=IS_PRODUCTION,
            samesite='none', # type: ignore
            path='/',
        )

        # Set new refresh token in cookie
        res.set_cookie(
            key="refresh_token",
            value=new_refresh_token,
            max_age=7 * 24 * 60 * 60,  # 7 days
            httponly=True,
            secure=IS_PRODUCTION,  # Set to False for local development
            samesite="none",  # CSRF protection
            path="/api/user/refresh-token",
        )

        logger.info(f"Access token refreshed for email: {user['email']}")
        return {
            "success": True,
            "message": "Token đã được làm mới thành công.",
            "user": {
                "username": user['username'], # Lấy username, fallback về email
                "email": user['email'],
                "role": user['role'],
                "avatar_url": user['avatar_url']
            }
            # Không cần trả về token trong body nếu chúng đã được set trong HttpOnly cookies
        }

    except HTTPException as he:
        raise he
    except Exception as e:
        logger.error(f"Lỗi khi làm mới access token: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Lỗi hệ thống khi làm mới access token"
        )


async def verify_forgot_password_code(email: str, code: str) -> dict:

    try:
        # Find user by email and code
        user =await  mongo_db.users.find_one({
            "email": email.lower(),
            "reset_password_code": code
        })
        if not user:
            logger.warning(f"Mã xác minh không hợp lệ: {code}")
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Mã xác minh không hợp lệ"
            )

        # Check if code is expired
        expiry = user.get("reset_password_expiry")
        if not expiry or expiry < datetime.now():
            logger.warning(f"Mã xác minh đã hết hạn cho email: {email}")
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Mã xác minh đã hết hạn"
            )



        return True
    except HTTPException as he:
        raise he
    except Exception as e:
        logger.error(f"Lỗi khi xác minh mã quen mat khau: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Lỗi hệ thống khi xác minh mã quen mat khau."
        )