185 lines
6.3 KiB
Markdown
185 lines
6.3 KiB
Markdown
# 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 */}
|
|
<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](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.
|