# 後端整合 (/guides/portal/server-integration)





後端整合 [#後端整合]

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

安裝 SDK [#安裝-sdk]

<Tabs items="['npm', 'pnpm', 'yarn']">
  <Tab value="npm">
    ```bash
    npm install recur-tw
    ```
  </Tab>

  <Tab value="pnpm">
    ```bash
    pnpm add recur-tw
    ```
  </Tab>

  <Tab value="yarn">
    ```bash
    yarn add recur-tw
    ```
  </Tab>
</Tabs>

初始化 SDK [#初始化-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 [#建立-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 [#nextjs-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 [#expressjs]

```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-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:
        // 其他錯誤
    }
  }
}
```

安全性最佳實踐 [#安全性最佳實踐]

<Callout type="warning">
  **務必遵循以下安全性原則**
</Callout>

1\. 驗證用戶身份 [#1-驗證用戶身份]

在建立 Portal Session 前，務必驗證用戶已登入：

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

2\. 驗證客戶所有權 [#2-驗證客戶所有權]

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

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

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

3\. 保護 Secret Key [#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/portal) - 完整 API 文件
