# Plan: Đăng nhập / Đăng ký bằng Tài khoản Google (Google OAuth) Cho phép người dùng tạo tài khoản hoặc đăng nhập nhanh chóng bằng tài khoản Google đang được sử dụng trên thiết bị, không cần nhập email/mật khẩu thủ công. --- ## Luồng hoạt động ``` 1. Người dùng nhấn nút "Đăng nhập bằng Google" 2. Frontend redirect → Backend endpoint /api/v1/auth/google 3. Backend redirect → Google OAuth Consent Screen 4. Google xác thực xong → callback về /api/v1/auth/google/callback 5. Backend tìm user theo googleId (hoặc email): - Nếu đã tồn tại → đăng nhập, cấp JWT - Nếu chưa có → tạo tài khoản mới, cấp JWT 6. Backend redirect về Frontend kèm JWT trong query param 7. Frontend đọc JWT → lưu localStorage → điều hướng vào app ``` --- ## Open Questions > [!IMPORTANT] > Trước khi thực hiện, bạn cần: > 1. Tạo **Google OAuth 2.0 Client** tại [Google Cloud Console](https://console.cloud.google.com/apis/credentials) > 2. Điền **Authorized redirect URI**: `http://localhost:3001/api/v1/auth/google/callback` > 3. Cung cấp `GOOGLE_CLIENT_ID` và `GOOGLE_CLIENT_SECRET` để thêm vào `.env` --- ## Proposed Changes ### Backend --- #### [MODIFY] [schema.prisma](file:///home/locpham/travelplanning/backend/prisma/schema.prisma) - Thêm trường `googleId String? @unique` vào model `User` để lưu Google Account ID (unique identifier của từng tài khoản Google). ```diff model User { id String @id @default(uuid()) email String? @unique + googleId String? @unique passwordHash String? ... ``` - Chạy migration: `npm run db:migrate` --- #### [MODIFY] [main.ts](file:///home/locpham/travelplanning/backend/src/main.ts) **1. Cài đặt thêm dependencies:** ```bash npm install passport-google-oauth20 @types/passport-google-oauth20 -w backend ``` **2. Import & cấu hình Google Strategy (thêm vào đầu file):** ```typescript import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; import * as passport from 'passport'; ``` **3. Thêm 2 endpoint mới vào `AuthController`:** - `GET /auth/google` — Khởi động OAuth flow, redirect sang Google - `GET /auth/google/callback` — Google callback; tìm/tạo user, cấp JWT rồi redirect về frontend với token ```typescript @Get('google') async googleAuth(@Req() req: any, @Res() res: any) { // Redirect đến Google login const params = new URLSearchParams({ client_id: process.env.GOOGLE_CLIENT_ID!, redirect_uri: `${process.env.BACKEND_URL}/api/v1/auth/google/callback`, response_type: 'code', scope: 'email profile', access_type: 'offline', }); res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`); } @Get('google/callback') async googleCallback(@Query('code') code: string, @Res() res: any) { // 1. Exchange code for tokens với Google // 2. Lấy profile (googleId, email, name, picture) // 3. Upsert user theo googleId hoặc email // 4. Tạo JWT // 5. Redirect về frontend: http://localhost:5173/auth/callback?token=...&user=... } ``` > [!NOTE] > Không dùng `passport.authenticate()` middleware để đơn giản hóa, thay vào đó dùng `fetch` trực tiếp tới Google Token endpoint để trao đổi `code` lấy `access_token`, sau đó gọi Google People API để lấy profile. **4. Thêm biến môi trường mới vào `.env`:** ```env GOOGLE_CLIENT_ID=your_client_id GOOGLE_CLIENT_SECRET=your_client_secret BACKEND_URL=http://localhost:3001 FRONTEND_URL=http://localhost:5173 ``` --- ### Frontend --- #### [MODIFY] [App.tsx](file:///home/locpham/travelplanning/frontend/src/App.tsx) - Thêm xử lý route `/auth/callback` (hoặc dùng `useEffect` kiểm tra query params khi app mount): đọc `?token=` và `?user=` từ URL, lưu vào `localStorage`, sau đó điều hướng vào trang explore. --- #### [MODIFY] [LoginModal.tsx](file:///home/locpham/travelplanning/frontend/src/components/LoginModal.tsx) - Thêm nút **"Đăng nhập bằng Google"** ở phía dưới form, tách biệt bằng divider `— hoặc —`. - Khi nhấn: `window.location.href = '/api/v1/auth/google'` ```tsx {/* Divider */}
hoặc
{/* Google Button */} ``` --- #### [MODIFY] [SignupPage.tsx](file:///home/locpham/travelplanning/frontend/src/pages/SignupPage.tsx) - Thêm nút "Đăng ký nhanh bằng Google" tương tự ở phần `step === 'form'`. - Cùng hành động: redirect sang `/api/v1/auth/google`. --- ## Sơ đồ kiến trúc ```mermaid sequenceDiagram participant U as Người dùng participant FE as Frontend participant BE as Backend participant G as Google OAuth U->>FE: Nhấn "Đăng nhập bằng Google" FE->>BE: GET /api/v1/auth/google BE->>G: Redirect → Google Consent Screen G->>U: Hiển thị chọn tài khoản U->>G: Chọn tài khoản G->>BE: Callback với authorization code BE->>G: Exchange code → access_token BE->>G: GET profile (googleId, email, name, avatar) BE->>BE: Upsert User (tìm theo googleId hoặc email) BE->>BE: Tạo JWT BE->>FE: Redirect về /auth/callback?token=...&user=... FE->>FE: Lưu token/user vào localStorage FE->>U: Điều hướng vào Explore Map ``` --- ## Verification Plan ### Automated Tests - `npm run build -w frontend` để kiểm tra TypeScript. ### Manual Verification 1. Nhấn "Đăng nhập bằng Google" → chọn tài khoản Google → kiểm tra được điều hướng vào app. 2. Đăng xuất → đăng nhập lại bằng cùng tài khoản Google → kiểm tra không bị tạo user mới. 3. Người dùng đã có tài khoản email trùng → kiểm tra được hợp nhất (merge) với tài khoản hiện có. 4. Kiểm tra `googleId` được lưu vào DB.