# 示例：微信公众平台（Stable Token）

## 1) 创建平台

```bash
curl -X POST http://127.0.0.1:8000/v1/platforms \
  -H 'content-type: application/json' \
  -d '{
    "id": "plat_wechat_official",
    "orgId": "org_wechat",
    "nameId": "wechat_official",
    "name": "微信公众平台",
    "code": "wechat_official"
  }'
```

## 2) 创建授权方式（OAuth1 语义，stable_token 流程）

```bash
curl -X POST http://127.0.0.1:8000/v1/auth-methods \
  -H 'content-type: application/json' \
  -d '{
    "id": "am_wechat_official_oauth1",
    "platformId": "plat_wechat_official",
    "name": "wechat_official_oauth1",
    "type": "OAuth1",
    "description": "微信公众平台稳定版 access_token 获取流程，两步：配置账号与调用 stable_token 接口。",
    "loginFieldsSchema": {
      "type": "object",
      "required": ["appid", "secret"],
      "properties": {
        "appid": { "type": "string", "title": "AppID" },
        "secret": { "type": "string", "title": "AppSecret" },
        "grant_type": { "type": "string", "title": "grant_type", "default": "client_credential" },
        "force_refresh": { "type": "boolean", "title": "force_refresh", "default": false },
        "callback_url": { "type": "string", "title": "回调地址(只读)", "readOnly": true, "description": "由服务端环境变量注入，禁止人工填写" }
      }
    },
    "loginFlow": {
      "url": "https://api.weixin.qq.com/cgi-bin/stable_token",
      "method": "POST",
      "headers": { "Content-Type": "application/json" },
      "body_template": {
        "grant_type": "{{grant_type}}",
        "appid": "{{appid}}",
        "secret": "{{secret}}",
        "force_refresh": "{{force_refresh}}"
      }
    },
    "responseMapping": {
      "fields": {
        "access_token": "access_token",
        "expires_in": "expires_in"
      }
    },
    "authFieldPlacements": {
      "query": { "access_token": { "sourceField": "access_token" } },
      "headers": { "Authorization": { "type": "Bearer", "tokenField": "access_token" } }
    },
    "defaultValiditySeconds": 7200,
    "refreshBeforeSeconds": 300
  }'
```

## 3) 创建 Secret 并触发自动登录

```bash
curl -X POST http://127.0.0.1:8000/v1/auth-methods/am_wechat_official_oauth1/secret \
  -H 'content-type: application/json' \
  -d '{
    "id": "sec_wechat_official_demo",
    "name": "wechat_official_demo",
    "tags": ["wechat", "official"],
    "data": {},
    "loginPayload": {
      "appid": "<替换为公众号AppID>",
      "secret": "<替换为公众号AppSecret>",
      "force_refresh": false
    },
    "autoLoginEnabled": true
  }'
```

# 客户端应用 `usageMapping`

本示例演示如何在客户端获取 Secret 并根据返回的 `usageMapping` 将凭证注入到业务请求中。

假设微信侧最终需要把 `access_token` 放到查询参数 `access_token` 中，或用 `Authorization: Bearer <token>` 头；`AuthMethod.authFieldPlacements` 可配置如下（服务端会在 Secret 响应中透出为 `usageMapping`）：

```json
{
  "query": {
    "access_token": { "sourceField": "access_token" }
  },
  "headers": {
    "Authorization": { "type": "Bearer", "tokenField": "access_token" }
  }
}
```

## Python（requests + 通用映射器）

```python
import requests
from typing import Any, Dict

# 1) 获取 Secret（含 usageMapping）
base = "http://127.0.0.1:8000"
secret_id = "<your-secret-id>"
resp = requests.get(f"{base}/v1/secrets/{secret_id}")
resp.raise_for_status()
secret = resp.json()

secret_data: Dict[str, Any] = secret.get("data", {})
usage_mapping: Dict[str, Any] | None = secret.get("usageMapping")
secret_type = secret.get("type")  # 例如 'OAuth1' 或 'OAuth2'，本示例按 usageMapping 构造

# 2) 将 usageMapping 应用到一次业务请求的 headers/query/cookies/body

def get_by_path(obj: Dict[str, Any], path: str) -> Any:
    cur: Any = obj
    for p in path.split('.'):
        if isinstance(cur, dict) and p in cur:
            cur = cur[p]
        else:
            return None
    return cur


def apply_usage_mapping(mapping: Dict[str, Any], data: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
    result = {"headers": {}, "query": {}, "cookies": {}, "body": {}}

    def resolve_value(spec: Any) -> Any:
        if isinstance(spec, str):
            # 字符串按路径取值
            return get_by_path(data, spec) if "." in spec or spec in data else spec
        if isinstance(spec, dict):
            if "value" in spec:
                return spec["value"]
            if "tokenField" in spec:
                token = get_by_path(data, spec["tokenField"]) or data.get(spec["tokenField"]) 
                if token is None:
                    return None
                t = spec.get("type")
                if t:
                    return f"{t} {token}"
                fmt = spec.get("format")
                return fmt.replace("{{value}}", str(token)) if fmt else token
            if "sourceField" in spec:
                val = get_by_path(data, spec["sourceField"]) or data.get(spec["sourceField"]) 
                if val is None:
                    return None
                fmt = spec.get("format")
                return fmt.replace("{{value}}", str(val)) if fmt else val
        return spec

    for section in ("headers", "query", "cookies", "body"):
        conf = mapping.get(section)
        if isinstance(conf, dict):
            for k, v in conf.items():
                rv = resolve_value(v)
                if rv is not None:
                    result[section][k] = rv

    return result

applied = apply_usage_mapping(usage_mapping or {}, secret_data)

# 3) 发起业务请求（示例 GET）
url = "https://api.weixin.qq.com/cgi-bin/some_api"
r = requests.get(url, headers=applied["headers"], params=applied["query"], cookies=applied["cookies"])  # body 按需传
print(r.status_code, r.text)
```

## JavaScript（fetch + 通用映射器）

```js
async function getSecret(secretId) {
  const res = await fetch(`http://127.0.0.1:8000/v1/secrets/${secretId}`);
  if (!res.ok) throw new Error("failed to load secret");
  return res.json();
}

function getByPath(obj, path) {
  return path.split('.').reduce((cur, p) => (cur && cur[p] !== undefined ? cur[p] : undefined), obj);
}

function applyUsageMapping(mapping = {}, data = {}) {
  const result = { headers: {}, query: {}, cookies: {}, body: {} };
  const resolveValue = (spec) => {
    if (typeof spec === 'string') {
      return spec.includes('.') || data[spec] !== undefined ? (getByPath(data, spec) ?? data[spec]) : spec;
    }
    if (spec && typeof spec === 'object') {
      if ('value' in spec) return spec.value;
      if ('tokenField' in spec) {
        const token = getByPath(data, spec.tokenField) ?? data[spec.tokenField];
        if (token == null) return undefined;
        if (spec.type) return `${spec.type} ${token}`;
        return spec.format ? spec.format.replace('{{value}}', String(token)) : token;
      }
      if ('sourceField' in spec) {
        const val = getByPath(data, spec.sourceField) ?? data[spec.sourceField];
        if (val == null) return undefined;
        return spec.format ? spec.format.replace('{{value}}', String(val)) : val;
      }
    }
    return spec;
  };

  for (const section of ['headers', 'query', 'cookies', 'body']) {
    const conf = mapping[section];
    if (conf && typeof conf === 'object') {
      for (const [k, v] of Object.entries(conf)) {
        const rv = resolveValue(v);
        if (rv !== undefined) result[section][k] = rv;
      }
    }
  }
  return result;
}

(async () => {
  const secret = await getSecret('<your-secret-id>');
  // 可根据 secret.type 决定构造方式（如 OAuth1/OAuth2/Cookies），本示例直接套用 usageMapping
  const applied = applyUsageMapping(secret.usageMapping || {}, secret.data || {});
  const url = new URL('https://api.weixin.qq.com/cgi-bin/some_api');
  Object.entries(applied.query).forEach(([k, v]) => url.searchParams.set(k, v));
  const res = await fetch(url.toString(), { headers: applied.headers, credentials: 'include' });
  console.log(res.status, await res.text());
})();
```
