Files
travelplanning/google_oauth_plan.md
T

6.3 KiB

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
  2. Điền Authorized redirect URI: http://localhost:3001/api/v1/auth/google/callback
  3. Cung cấp GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRET để thêm vào .env

Proposed Changes

Backend


[MODIFY] 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).
 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

1. Cài đặt thêm dependencies:

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):

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
@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:

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

  • Thêm xử lý route /auth/callback (hoặc dùng useEffect kiểm tra query params khi app mount): đọc ?token=?user= từ URL, lưu vào localStorage, sau đó điều hướng vào trang explore.

[MODIFY] 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'
{/* Divider */}
<div className="flex items-center gap-3 my-4">
  <div className="flex-1 border-t border-gray-200" />
  <span className="text-xs text-gray-400 font-medium">hoặc</span>
  <div className="flex-1 border-t border-gray-200" />
</div>

{/* Google Button */}
<button
  type="button"
  onClick={() => window.location.href = '/api/v1/auth/google'}
  className="w-full flex items-center justify-center gap-3 bg-white border border-gray-200 hover:bg-gray-50 text-gray-700 font-semibold py-4 rounded-2xl shadow-sm transition-all"
>
  <GoogleIcon />
  Đăng nhập bằng Google
</button>

[MODIFY] 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

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.