# 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 */}