fix: build ứng dụng trên android và có thể đăng nhập bằng Google Oauth

This commit is contained in:
2026-06-27 10:39:22 +07:00
parent d8bbb22dbd
commit ee42bfcefa
47 changed files with 5149 additions and 124 deletions
+184
View File
@@ -0,0 +1,184 @@
# 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=``?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.