feat: HR Portal - Complete Multi-Tenant System with Redis Session Storage
Major Features: - ✅ Multi-tenant architecture (tenant isolation) - ✅ Employee CRUD with lifecycle management (onboarding/offboarding) - ✅ Department tree structure with email domain management - ✅ Company info management (single-record editing) - ✅ System functions CRUD (permission management) - ✅ Email account management (multi-account per employee) - ✅ Keycloak SSO integration (auth.lab.taipei) - ✅ Redis session storage (10.1.0.254:6379) - Solves Cookie 4KB limitation - Cross-system session sharing - Sliding expiration (8 hours) - Automatic token refresh Technical Stack: Backend: - FastAPI + SQLAlchemy - PostgreSQL 16 (10.1.0.20:5433) - Keycloak Admin API integration - Docker Mailserver integration (SSH) - Alembic migrations Frontend: - Next.js 14 (App Router) - NextAuth 4 with Keycloak Provider - Redis session storage (ioredis) - Tailwind CSS Infrastructure: - Redis 7 (10.1.0.254:6379) - Session + Cache - Keycloak 26.1.0 (auth.lab.taipei) - Docker Mailserver (10.1.0.254) Architecture Highlights: - Session管理由 Keycloak + Redis 統一控制 - 支援多系統 (HR/WebMail/Calendar/Drive/Office) 共享 session - Token 自動刷新,異質服務整合 - 未來可無縫遷移到雲端 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
641
GOOGLE-INTEGRATION.md
Normal file
641
GOOGLE-INTEGRATION.md
Normal file
@@ -0,0 +1,641 @@
|
||||
# 🔗 Google Workspace 整合指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
整合 Google Calendar 和 Google Meet 到 HR Portal,讓員工可以:
|
||||
- 📅 雙向同步 Google Calendar
|
||||
- 🎥 透過您的 Google 帳號建立 Google Meet 會議
|
||||
- 📧 自動發送會議邀請
|
||||
- 🔔 接收 Google 的會議提醒
|
||||
|
||||
---
|
||||
|
||||
## 🎯 整合方案
|
||||
|
||||
### 方案 A: Google Workspace API (推薦) ⭐
|
||||
|
||||
**使用您的 Google One 帳號作為服務帳號**
|
||||
|
||||
**優點**:
|
||||
- ✅ 直接使用您現有的 Google 帳號
|
||||
- ✅ 完整的 Google Meet 功能
|
||||
- ✅ 無需額外付費
|
||||
- ✅ Google Calendar 原生體驗
|
||||
|
||||
**需要的 API**:
|
||||
1. **Google Calendar API** - 行事曆同步
|
||||
2. **Google Meet API** - 建立會議
|
||||
3. **Google People API** - 聯絡人管理 (可選)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 設定步驟
|
||||
|
||||
### 步驟 1: 建立 Google Cloud Project
|
||||
|
||||
1. 訪問 [Google Cloud Console](https://console.cloud.google.com/)
|
||||
|
||||
2. 建立新專案
|
||||
```
|
||||
專案名稱: HR Portal Integration
|
||||
組織: (您的組織或個人)
|
||||
```
|
||||
|
||||
3. 啟用 API
|
||||
- Google Calendar API
|
||||
- Google Meet API (部分 Google Workspace Admin API)
|
||||
- Google People API
|
||||
|
||||
### 步驟 2: 建立 OAuth 2.0 憑證
|
||||
|
||||
1. **API 和服務 → 憑證**
|
||||
|
||||
2. **建立憑證 → OAuth 用戶端 ID**
|
||||
```
|
||||
應用程式類型: 網頁應用程式
|
||||
名稱: HR Portal
|
||||
|
||||
已授權的 JavaScript 來源:
|
||||
- https://hr.porscheworld.tw
|
||||
- http://localhost:3000 (開發用)
|
||||
|
||||
已授權的重新導向 URI:
|
||||
- https://hr.porscheworld.tw/api/v1/auth/google/callback
|
||||
- http://localhost:3000/auth/google/callback (開發用)
|
||||
```
|
||||
|
||||
3. **下載 JSON 憑證檔案**
|
||||
- 檔名: `google-credentials.json`
|
||||
- 儲存到: `backend/secrets/`
|
||||
|
||||
4. **記錄以下資訊**:
|
||||
```
|
||||
Client ID: xxxxx.apps.googleusercontent.com
|
||||
Client Secret: xxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
### 步驟 3: 設定 OAuth 同意畫面
|
||||
|
||||
1. **OAuth 同意畫面**
|
||||
```
|
||||
使用者類型: 外部 (或內部,如果有 Google Workspace)
|
||||
|
||||
應用程式資訊:
|
||||
- 應用程式名稱: HR Portal
|
||||
- 使用者支援電子郵件: admin@porscheworld.tw
|
||||
- 應用程式標誌: (您的 Logo)
|
||||
|
||||
授權網域:
|
||||
- porscheworld.tw
|
||||
|
||||
開發人員聯絡資訊:
|
||||
- it@porscheworld.tw
|
||||
```
|
||||
|
||||
2. **範圍 (Scopes)**
|
||||
```
|
||||
新增以下範圍:
|
||||
- https://www.googleapis.com/auth/calendar
|
||||
- https://www.googleapis.com/auth/calendar.events
|
||||
- https://www.googleapis.com/auth/meetings.space.created
|
||||
- https://www.googleapis.com/auth/userinfo.email
|
||||
- https://www.googleapis.com/auth/userinfo.profile
|
||||
```
|
||||
|
||||
3. **測試使用者** (如果應用程式處於測試階段)
|
||||
- 新增公司員工的 Gmail 地址
|
||||
|
||||
---
|
||||
|
||||
## 💻 後端實作
|
||||
|
||||
### 1. 安裝相關套件
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client
|
||||
```
|
||||
|
||||
更新 `requirements.txt`:
|
||||
```txt
|
||||
google-auth==2.26.2
|
||||
google-auth-oauthlib==1.2.0
|
||||
google-auth-httplib2==0.2.0
|
||||
google-api-python-client==2.114.0
|
||||
```
|
||||
|
||||
### 2. 環境變數設定
|
||||
|
||||
編輯 `backend/.env`:
|
||||
```env
|
||||
# Google OAuth
|
||||
GOOGLE_CLIENT_ID=xxxxx.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=xxxxxxxxxxxxxx
|
||||
GOOGLE_REDIRECT_URI=https://hr.porscheworld.tw/api/v1/auth/google/callback
|
||||
|
||||
# Google API Scopes
|
||||
GOOGLE_SCOPES=https://www.googleapis.com/auth/calendar,https://www.googleapis.com/auth/calendar.events
|
||||
|
||||
# 服務帳號 (您的 Google One 帳號)
|
||||
GOOGLE_SERVICE_ACCOUNT_EMAIL=porsche.chen@gmail.com
|
||||
```
|
||||
|
||||
### 3. Google 服務類別
|
||||
|
||||
創建 `backend/app/services/google_service.py`:
|
||||
|
||||
```python
|
||||
"""
|
||||
Google Workspace 整合服務
|
||||
"""
|
||||
from google.oauth2.credentials import Credentials
|
||||
from google_auth_oauthlib.flow import Flow
|
||||
from googleapiclient.discovery import build
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Optional
|
||||
import os
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
class GoogleService:
|
||||
"""Google API 服務"""
|
||||
|
||||
def __init__(self, credentials: Credentials):
|
||||
self.credentials = credentials
|
||||
self.calendar_service = build('calendar', 'v3', credentials=credentials)
|
||||
|
||||
@staticmethod
|
||||
def get_auth_url(state: str) -> str:
|
||||
"""
|
||||
取得 Google OAuth 授權 URL
|
||||
|
||||
Args:
|
||||
state: 狀態參數 (用於防止 CSRF)
|
||||
|
||||
Returns:
|
||||
授權 URL
|
||||
"""
|
||||
flow = Flow.from_client_config(
|
||||
{
|
||||
"web": {
|
||||
"client_id": settings.GOOGLE_CLIENT_ID,
|
||||
"client_secret": settings.GOOGLE_CLIENT_SECRET,
|
||||
"redirect_uris": [settings.GOOGLE_REDIRECT_URI],
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
}
|
||||
},
|
||||
scopes=settings.GOOGLE_SCOPES.split(',')
|
||||
)
|
||||
|
||||
flow.redirect_uri = settings.GOOGLE_REDIRECT_URI
|
||||
authorization_url, _ = flow.authorization_url(
|
||||
access_type='offline',
|
||||
include_granted_scopes='true',
|
||||
state=state,
|
||||
prompt='consent'
|
||||
)
|
||||
|
||||
return authorization_url
|
||||
|
||||
@staticmethod
|
||||
def get_credentials_from_code(code: str) -> Credentials:
|
||||
"""
|
||||
用授權碼換取憑證
|
||||
|
||||
Args:
|
||||
code: OAuth 授權碼
|
||||
|
||||
Returns:
|
||||
Google Credentials
|
||||
"""
|
||||
flow = Flow.from_client_config(
|
||||
{
|
||||
"web": {
|
||||
"client_id": settings.GOOGLE_CLIENT_ID,
|
||||
"client_secret": settings.GOOGLE_CLIENT_SECRET,
|
||||
"redirect_uris": [settings.GOOGLE_REDIRECT_URI],
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
}
|
||||
},
|
||||
scopes=settings.GOOGLE_SCOPES.split(',')
|
||||
)
|
||||
|
||||
flow.redirect_uri = settings.GOOGLE_REDIRECT_URI
|
||||
flow.fetch_token(code=code)
|
||||
|
||||
return flow.credentials
|
||||
|
||||
# === Google Calendar Methods ===
|
||||
|
||||
def list_events(
|
||||
self,
|
||||
time_min: Optional[datetime] = None,
|
||||
time_max: Optional[datetime] = None,
|
||||
max_results: int = 100
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
列出 Google Calendar 事件
|
||||
|
||||
Args:
|
||||
time_min: 開始時間
|
||||
time_max: 結束時間
|
||||
max_results: 最大結果數
|
||||
|
||||
Returns:
|
||||
事件列表
|
||||
"""
|
||||
if not time_min:
|
||||
time_min = datetime.utcnow()
|
||||
if not time_max:
|
||||
time_max = time_min + timedelta(days=30)
|
||||
|
||||
events_result = self.calendar_service.events().list(
|
||||
calendarId='primary',
|
||||
timeMin=time_min.isoformat() + 'Z',
|
||||
timeMax=time_max.isoformat() + 'Z',
|
||||
maxResults=max_results,
|
||||
singleEvents=True,
|
||||
orderBy='startTime'
|
||||
).execute()
|
||||
|
||||
return events_result.get('items', [])
|
||||
|
||||
def create_event(
|
||||
self,
|
||||
summary: str,
|
||||
start_time: datetime,
|
||||
end_time: datetime,
|
||||
description: Optional[str] = None,
|
||||
location: Optional[str] = None,
|
||||
attendees: Optional[List[str]] = None,
|
||||
add_meet: bool = True
|
||||
) -> Dict:
|
||||
"""
|
||||
建立 Google Calendar 事件
|
||||
|
||||
Args:
|
||||
summary: 事件標題
|
||||
start_time: 開始時間
|
||||
end_time: 結束時間
|
||||
description: 事件描述
|
||||
location: 地點
|
||||
attendees: 參與者 email 列表
|
||||
add_meet: 是否新增 Google Meet 連結
|
||||
|
||||
Returns:
|
||||
建立的事件資料
|
||||
"""
|
||||
event = {
|
||||
'summary': summary,
|
||||
'start': {
|
||||
'dateTime': start_time.isoformat(),
|
||||
'timeZone': 'Asia/Taipei',
|
||||
},
|
||||
'end': {
|
||||
'dateTime': end_time.isoformat(),
|
||||
'timeZone': 'Asia/Taipei',
|
||||
},
|
||||
}
|
||||
|
||||
if description:
|
||||
event['description'] = description
|
||||
|
||||
if location:
|
||||
event['location'] = location
|
||||
|
||||
if attendees:
|
||||
event['attendees'] = [{'email': email} for email in attendees]
|
||||
|
||||
# 新增 Google Meet
|
||||
if add_meet:
|
||||
event['conferenceData'] = {
|
||||
'createRequest': {
|
||||
'requestId': f"meet-{datetime.utcnow().timestamp()}",
|
||||
'conferenceSolutionKey': {'type': 'hangoutsMeet'}
|
||||
}
|
||||
}
|
||||
|
||||
# 建立事件
|
||||
created_event = self.calendar_service.events().insert(
|
||||
calendarId='primary',
|
||||
body=event,
|
||||
conferenceDataVersion=1 if add_meet else 0,
|
||||
sendUpdates='all' # 發送邀請給所有參與者
|
||||
).execute()
|
||||
|
||||
return created_event
|
||||
|
||||
def update_event(
|
||||
self,
|
||||
event_id: str,
|
||||
summary: Optional[str] = None,
|
||||
start_time: Optional[datetime] = None,
|
||||
end_time: Optional[datetime] = None,
|
||||
description: Optional[str] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
更新 Google Calendar 事件
|
||||
|
||||
Args:
|
||||
event_id: Google Event ID
|
||||
summary: 新標題
|
||||
start_time: 新開始時間
|
||||
end_time: 新結束時間
|
||||
description: 新描述
|
||||
|
||||
Returns:
|
||||
更新後的事件資料
|
||||
"""
|
||||
# 取得現有事件
|
||||
event = self.calendar_service.events().get(
|
||||
calendarId='primary',
|
||||
eventId=event_id
|
||||
).execute()
|
||||
|
||||
# 更新欄位
|
||||
if summary:
|
||||
event['summary'] = summary
|
||||
if start_time:
|
||||
event['start']['dateTime'] = start_time.isoformat()
|
||||
if end_time:
|
||||
event['end']['dateTime'] = end_time.isoformat()
|
||||
if description:
|
||||
event['description'] = description
|
||||
|
||||
# 更新事件
|
||||
updated_event = self.calendar_service.events().update(
|
||||
calendarId='primary',
|
||||
eventId=event_id,
|
||||
body=event,
|
||||
sendUpdates='all'
|
||||
).execute()
|
||||
|
||||
return updated_event
|
||||
|
||||
def delete_event(self, event_id: str):
|
||||
"""
|
||||
刪除 Google Calendar 事件
|
||||
|
||||
Args:
|
||||
event_id: Google Event ID
|
||||
"""
|
||||
self.calendar_service.events().delete(
|
||||
calendarId='primary',
|
||||
eventId=event_id,
|
||||
sendUpdates='all'
|
||||
).execute()
|
||||
|
||||
def get_meet_link(self, event_id: str) -> Optional[str]:
|
||||
"""
|
||||
取得 Google Meet 連結
|
||||
|
||||
Args:
|
||||
event_id: Google Event ID
|
||||
|
||||
Returns:
|
||||
Meet 連結或 None
|
||||
"""
|
||||
event = self.calendar_service.events().get(
|
||||
calendarId='primary',
|
||||
eventId=event_id
|
||||
).execute()
|
||||
|
||||
conference_data = event.get('conferenceData', {})
|
||||
entry_points = conference_data.get('entryPoints', [])
|
||||
|
||||
for entry in entry_points:
|
||||
if entry.get('entryPointType') == 'video':
|
||||
return entry.get('uri')
|
||||
|
||||
return None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 認證流程
|
||||
|
||||
### 1. 員工首次連接 Google 帳號
|
||||
|
||||
```
|
||||
員工點擊「連接 Google Calendar」
|
||||
↓
|
||||
重定向到 Google 授權頁面
|
||||
↓
|
||||
員工登入並授權
|
||||
↓
|
||||
Google 重定向回 HR Portal (帶授權碼)
|
||||
↓
|
||||
後端用授權碼換取 Access Token
|
||||
↓
|
||||
儲存 Token 到資料庫 (加密)
|
||||
↓
|
||||
開始同步 Google Calendar
|
||||
```
|
||||
|
||||
### 2. 建立會議流程
|
||||
|
||||
```
|
||||
員工在 HR Portal 建立會議
|
||||
↓
|
||||
選擇「使用 Google Meet」
|
||||
↓
|
||||
後端呼叫 Google Calendar API
|
||||
↓
|
||||
自動建立 Calendar 事件 + Google Meet 連結
|
||||
↓
|
||||
發送邀請給參與者
|
||||
↓
|
||||
返回 Meet 連結給前端
|
||||
↓
|
||||
員工可直接點擊加入會議
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 資料庫擴充
|
||||
|
||||
新增欄位到 `employees` 表:
|
||||
|
||||
```sql
|
||||
ALTER TABLE employees ADD COLUMN IF NOT EXISTS
|
||||
google_calendar_connected BOOLEAN DEFAULT FALSE,
|
||||
google_access_token TEXT, -- 加密儲存
|
||||
google_refresh_token TEXT, -- 加密儲存
|
||||
google_token_expiry TIMESTAMP,
|
||||
google_calendar_id VARCHAR(255),
|
||||
last_google_sync TIMESTAMP;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 前端整合
|
||||
|
||||
### React 範例 - 連接 Google Calendar
|
||||
|
||||
```typescript
|
||||
// 連接 Google Calendar 按鈕
|
||||
const ConnectGoogleButton = () => {
|
||||
const handleConnect = async () => {
|
||||
const response = await fetch('/api/v1/auth/google/authorize');
|
||||
const { auth_url } = await response.json();
|
||||
|
||||
// 開啟授權視窗
|
||||
window.location.href = auth_url;
|
||||
};
|
||||
|
||||
return (
|
||||
<button onClick={handleConnect}>
|
||||
<img src="/google-icon.svg" />
|
||||
連接 Google Calendar
|
||||
</button>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### React 範例 - 建立 Google Meet 會議
|
||||
|
||||
```typescript
|
||||
const CreateMeetingButton = () => {
|
||||
const createMeeting = async () => {
|
||||
const meeting = {
|
||||
title: "團隊會議",
|
||||
start_time: "2026-02-08T14:00:00+08:00",
|
||||
end_time: "2026-02-08T15:00:00+08:00",
|
||||
attendees: ["alice@ease.taipei", "bob@lab.taipei"],
|
||||
use_google_meet: true
|
||||
};
|
||||
|
||||
const response = await fetch('/api/v1/calendar/events', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(meeting)
|
||||
});
|
||||
|
||||
const event = await response.json();
|
||||
|
||||
// 顯示 Google Meet 連結
|
||||
alert(`會議已建立!\nGoogle Meet: ${event.hangout_link}`);
|
||||
};
|
||||
|
||||
return <button onClick={createMeeting}>建立 Google Meet 會議</button>;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 同步機制
|
||||
|
||||
### 雙向同步策略
|
||||
|
||||
1. **從 Google Calendar 同步到 HR Portal** (每 15 分鐘)
|
||||
- 使用 Google Calendar API 的 sync token
|
||||
- 只同步變更的事件
|
||||
- 更新本地資料庫
|
||||
|
||||
2. **從 HR Portal 同步到 Google Calendar** (即時)
|
||||
- 員工在 HR Portal 建立事件
|
||||
- 立即推送到 Google Calendar
|
||||
- 取得 Google Event ID 並儲存
|
||||
|
||||
3. **衝突處理**
|
||||
- Google 為主要來源
|
||||
- 顯示衝突警告
|
||||
- 讓員工選擇保留哪個版本
|
||||
|
||||
---
|
||||
|
||||
## ✨ 推薦的 UI/UX 套件
|
||||
|
||||
### 1. FullCalendar (推薦) ⭐
|
||||
|
||||
**網址**: https://fullcalendar.io/
|
||||
|
||||
**特色**:
|
||||
- ✅ 功能完整的行事曆元件
|
||||
- ✅ 支援日/週/月/議程視圖
|
||||
- ✅ 拖放事件
|
||||
- ✅ 與 Google Calendar 整合良好
|
||||
- ✅ React 版本: `@fullcalendar/react`
|
||||
|
||||
**安裝**:
|
||||
```bash
|
||||
npm install @fullcalendar/react @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction @fullcalendar/google-calendar
|
||||
```
|
||||
|
||||
**使用範例**:
|
||||
```typescript
|
||||
import FullCalendar from '@fullcalendar/react';
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import googleCalendarPlugin from '@fullcalendar/google-calendar';
|
||||
|
||||
function Calendar() {
|
||||
return (
|
||||
<FullCalendar
|
||||
plugins={[
|
||||
dayGridPlugin,
|
||||
timeGridPlugin,
|
||||
interactionPlugin,
|
||||
googleCalendarPlugin
|
||||
]}
|
||||
initialView="dayGridMonth"
|
||||
googleCalendarApiKey="YOUR_API_KEY"
|
||||
events={{
|
||||
googleCalendarId: 'primary'
|
||||
}}
|
||||
headerToolbar={{
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||
}}
|
||||
editable={true}
|
||||
selectable={true}
|
||||
select={handleDateSelect}
|
||||
eventClick={handleEventClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. React Big Calendar
|
||||
|
||||
**網址**: https://jquense.github.io/react-big-calendar/
|
||||
|
||||
**特色**:
|
||||
- ✅ 輕量級
|
||||
- ✅ 類似 Google Calendar 介面
|
||||
- ✅ 自訂性高
|
||||
|
||||
### 3. Ant Design Calendar
|
||||
|
||||
如果您使用 Ant Design:
|
||||
```typescript
|
||||
import { Calendar, Badge } from 'antd';
|
||||
|
||||
<Calendar dateCellRender={dateCellRender} />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 功能對比
|
||||
|
||||
| 功能 | Jitsi Meet (自架) | Google Meet (整合) |
|
||||
|------|------------------|-------------------|
|
||||
| 費用 | 免費 | 包含在 Google One |
|
||||
| 時間限制 | 無 | 24小時 |
|
||||
| 參與人數 | 視伺服器資源 | 100人 (Google One) |
|
||||
| 錄影 | 支援 | 支援 |
|
||||
| 螢幕分享 | 支援 | 支援 |
|
||||
| 背景模糊 | 支援 | 支援 |
|
||||
| 整合難度 | 中 | 低 |
|
||||
| 維護成本 | 需自行維護 | Google 維護 |
|
||||
|
||||
**建議**: 使用 Google Meet 整合,更簡單且功能完整! ⭐
|
||||
|
||||
---
|
||||
|
||||
**下一步我會建立完整的 Google 整合程式碼!** 🚀
|
||||
Reference in New Issue
Block a user