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>
642 lines
16 KiB
Markdown
642 lines
16 KiB
Markdown
# 🔗 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 整合程式碼!** 🚀
|