開發者指南Customer Portal
後端整合
使用 Recur Server SDK 整合 Customer Portal
後端整合
Customer Portal Session 必須在後端建立,以確保安全性。本指南說明如何使用 Recur Server SDK 整合 Portal。
安裝 SDK
npm install recur-twpnpm add recur-twyarn add recur-tw初始化 SDK
import Recur from 'recur-tw/server';
const recur = new Recur({
secretKey: process.env.RECUR_SECRET_KEY!,
// 可選:自訂 API URL(開發環境)
// baseUrl: 'http://localhost:3000/api',
});建立 Portal Session
基本用法
const session = await recur.portal.sessions.create({
customer: 'cus_xxxxx',
returnUrl: 'https://your-site.com/account',
});
console.log(session.url);
// https://portal.recur.tw/s/ps_xxxxx參數說明
| 參數 | 必填 | 說明 |
|---|---|---|
customer | ✅ | 客戶 ID |
returnUrl | ❌ | 離開 Portal 後的返回 URL |
configuration | ❌ | Portal 設定 ID |
locale | ❌ | 語言(zh-TW 或 en) |
回應格式
interface PortalSession {
id: string; // Portal Session ID
object: 'portal.session';
url: string; // Portal URL
customer: string; // 客戶 ID
returnUrl: string; // 返回 URL
status: 'active' | 'expired';
expiresAt: string; // 過期時間 (ISO 8601)
accessedAt: string | null;
createdAt: string;
}完整整合範例
Next.js App Router
// app/api/portal/create/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Recur from 'recur-tw/server';
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
const recur = new Recur({
secretKey: process.env.RECUR_SECRET_KEY!,
});
export async function POST(request: NextRequest) {
try {
// 1. 驗證用戶身份
const session = await auth();
if (!session?.user) {
return NextResponse.json(
{ error: { message: 'Unauthorized' } },
{ status: 401 }
);
}
// 2. 取得用戶對應的 Recur Customer ID
const user = await db.user.findUnique({
where: { id: session.user.id },
select: { recurCustomerId: true },
});
if (!user?.recurCustomerId) {
return NextResponse.json(
{ error: { message: 'No subscription found' } },
{ status: 404 }
);
}
// 3. 建立 Portal Session
const portalSession = await recur.portal.sessions.create({
customer: user.recurCustomerId,
returnUrl: `${process.env.NEXT_PUBLIC_URL}/account`,
});
// 4. 返回 Portal URL
return NextResponse.json({
url: portalSession.url,
});
} catch (error) {
console.error('Portal session error:', error);
return NextResponse.json(
{ error: { message: 'Failed to create portal session' } },
{ status: 500 }
);
}
}Express.js
// routes/portal.js
const express = require('express');
const Recur = require('recur-tw/server').default;
const { authenticateUser } = require('../middleware/auth');
const router = express.Router();
const recur = new Recur({
secretKey: process.env.RECUR_SECRET_KEY,
});
router.post('/create', authenticateUser, async (req, res) => {
try {
const { recurCustomerId } = req.user;
if (!recurCustomerId) {
return res.status(404).json({
error: { message: 'No subscription found' }
});
}
const session = await recur.portal.sessions.create({
customer: recurCustomerId,
returnUrl: `${process.env.APP_URL}/account`,
});
res.json({ url: session.url });
} catch (error) {
console.error('Portal session error:', error);
res.status(500).json({
error: { message: 'Failed to create portal session' }
});
}
});
module.exports = router;Python (Flask)
# routes/portal.py
from flask import Blueprint, jsonify, request
from functools import wraps
import requests
import os
portal = Blueprint('portal', __name__)
def require_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
# 實作您的認證邏輯
user = get_current_user()
if not user:
return jsonify({'error': {'message': 'Unauthorized'}}), 401
request.user = user
return f(*args, **kwargs)
return decorated
@portal.route('/create', methods=['POST'])
@require_auth
def create_portal_session():
customer_id = request.user.get('recur_customer_id')
if not customer_id:
return jsonify({'error': {'message': 'No subscription found'}}), 404
response = requests.post(
'https://api.recur.tw/v1/portal/sessions',
headers={
'Authorization': f'Bearer {os.environ["RECUR_SECRET_KEY"]}',
'Content-Type': 'application/json',
},
json={
'customerId': customer_id,
'returnUrl': f'{os.environ["APP_URL"]}/account',
}
)
if not response.ok:
return jsonify({'error': {'message': 'Failed to create portal session'}}), 500
data = response.json()
return jsonify({'url': data['url']})錯誤處理
import { RecurAPIError } from 'recur-tw/server';
try {
const session = await recur.portal.sessions.create({
customer: customerId,
returnUrl: returnUrl,
});
} catch (error) {
if (error instanceof RecurAPIError) {
console.error('API Error:', error.code, error.message);
switch (error.code) {
case 'customer_not_found':
// 客戶不存在
break;
case 'missing_return_url':
// 未設定返回 URL
break;
default:
// 其他錯誤
}
}
}安全性最佳實踐
務必遵循以下安全性原則
1. 驗證用戶身份
在建立 Portal Session 前,務必驗證用戶已登入:
const session = await auth();
if (!session?.user) {
return unauthorized();
}2. 驗證客戶所有權
確保用戶只能存取自己的 Portal:
// 不要直接使用前端傳入的 customerId
const customerId = req.body.customerId; // ❌ 不安全
// 應該從已驗證的用戶資料取得
const customerId = user.recurCustomerId; // ✅ 安全3. 保護 Secret Key
// ❌ 不要在前端暴露 Secret Key
const recur = new Recur({
secretKey: 'sk_live_xxxxx', // 不要硬編碼
});
// ✅ 使用環境變數
const recur = new Recur({
secretKey: process.env.RECUR_SECRET_KEY!,
});下一步
- 前端整合 - 使用 Web Component 建立 Portal 按鈕
- Portal API 參考 - 完整 API 文件