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>
16 KiB
16 KiB
🔗 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:
- Google Calendar API - 行事曆同步
- Google Meet API - 建立會議
- Google People API - 聯絡人管理 (可選)
🔧 設定步驟
步驟 1: 建立 Google Cloud Project
-
建立新專案
專案名稱: HR Portal Integration 組織: (您的組織或個人) -
啟用 API
- Google Calendar API
- Google Meet API (部分 Google Workspace Admin API)
- Google People API
步驟 2: 建立 OAuth 2.0 憑證
-
API 和服務 → 憑證
-
建立憑證 → 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 (開發用) -
下載 JSON 憑證檔案
- 檔名:
google-credentials.json - 儲存到:
backend/secrets/
- 檔名:
-
記錄以下資訊:
Client ID: xxxxx.apps.googleusercontent.com Client Secret: xxxxxxxxxxxxxx
步驟 3: 設定 OAuth 同意畫面
-
OAuth 同意畫面
使用者類型: 外部 (或內部,如果有 Google Workspace) 應用程式資訊: - 應用程式名稱: HR Portal - 使用者支援電子郵件: admin@porscheworld.tw - 應用程式標誌: (您的 Logo) 授權網域: - porscheworld.tw 開發人員聯絡資訊: - it@porscheworld.tw -
範圍 (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 -
測試使用者 (如果應用程式處於測試階段)
- 新增公司員工的 Gmail 地址
💻 後端實作
1. 安裝相關套件
cd backend
pip install google-auth google-auth-oauthlib google-auth-httplib2 google-api-python-client
更新 requirements.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:
# 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:
"""
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 表:
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
// 連接 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 會議
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>;
};
🔄 同步機制
雙向同步策略
-
從 Google Calendar 同步到 HR Portal (每 15 分鐘)
- 使用 Google Calendar API 的 sync token
- 只同步變更的事件
- 更新本地資料庫
-
從 HR Portal 同步到 Google Calendar (即時)
- 員工在 HR Portal 建立事件
- 立即推送到 Google Calendar
- 取得 Google Event ID 並儲存
-
衝突處理
- Google 為主要來源
- 顯示衝突警告
- 讓員工選擇保留哪個版本
✨ 推薦的 UI/UX 套件
1. FullCalendar (推薦) ⭐
特色:
- ✅ 功能完整的行事曆元件
- ✅ 支援日/週/月/議程視圖
- ✅ 拖放事件
- ✅ 與 Google Calendar 整合良好
- ✅ React 版本:
@fullcalendar/react
安裝:
npm install @fullcalendar/react @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction @fullcalendar/google-calendar
使用範例:
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:
import { Calendar, Badge } from 'antd';
<Calendar dateCellRender={dateCellRender} />
📊 功能對比
| 功能 | Jitsi Meet (自架) | Google Meet (整合) |
|---|---|---|
| 費用 | 免費 | 包含在 Google One |
| 時間限制 | 無 | 24小時 |
| 參與人數 | 視伺服器資源 | 100人 (Google One) |
| 錄影 | 支援 | 支援 |
| 螢幕分享 | 支援 | 支援 |
| 背景模糊 | 支援 | 支援 |
| 整合難度 | 中 | 低 |
| 維護成本 | 需自行維護 | Google 維護 |
建議: 使用 Google Meet 整合,更簡單且功能完整! ⭐
下一步我會建立完整的 Google 整合程式碼! 🚀