Recur
開發者指南Customer Portal

後端整合

使用 Recur Server SDK 整合 Customer Portal

後端整合

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

安裝 SDK

npm install recur-tw
pnpm add recur-tw
yarn 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
configurationPortal 設定 ID
locale語言(zh-TWen

回應格式

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

// 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!,
});

下一步

On this page