---
title: 後端整合
description: 使用 Recur Server SDK 整合 Customer Portal
---

# 後端整合

Customer Portal Session 必須在後端建立，以確保安全性。本指南說明如何使用 Recur Server SDK 整合 Portal。

## 安裝 SDK

```bash
npm install recur-tw
```

```bash
pnpm add recur-tw
```

```bash
yarn add recur-tw
```

## 初始化 SDK

```typescript
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

### 基本用法

```typescript
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`） |

### 回應格式

```typescript
interface PortalSession {
  id: string;                // Portal Session ID
  object: 'portal.session';
  url: string;               // Portal URL
  customer: string;          // 客戶 ID
  return_url: string;        // 返回 URL
  status: 'active' | 'expired';
  expires_at: string;        // 過期時間 (ISO 8601)
  accessed_at: string | null;
  created_at: string;
}
```

## 完整整合範例

### Next.js App Router

```typescript
// 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

```javascript
// 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)

```python
# 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']})
```

## 錯誤處理

```typescript
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 前，務必驗證用戶已登入：

```typescript
const session = await auth();
if (!session?.user) {
  return unauthorized();
}
```

### 2. 驗證客戶所有權

確保用戶只能存取自己的 Portal：

```typescript
// 不要直接使用前端傳入的 customerId
const customerId = req.body.customerId; // ❌ 不安全

// 應該從已驗證的用戶資料取得
const customerId = user.recurCustomerId; // ✅ 安全
```

### 3. 保護 Secret Key

```typescript
// ❌ 不要在前端暴露 Secret Key
const recur = new Recur({
  secretKey: 'sk_live_xxxxx', // 不要硬編碼
});

// ✅ 使用環境變數
const recur = new Recur({
  secretKey: process.env.RECUR_SECRET_KEY!,
});
```

## 下一步

- [前端整合](/guides/portal/client-integration) - 使用 Web Component 建立 Portal 按鈕
- [Portal API 參考](/api-reference/portal) - 完整 API 文件
