<template>
  <div class="help-content">
    <div class="content-header">
      <h2>OIDC (OpenID Connect) 集成指南</h2>
      <el-button type="primary" size="small" plain @click="downloadDoc('/docs/oidc_integration.md', 'OIDC_Integration_Guide.md')">
        <el-icon style="margin-right: 5px"><Download /></el-icon>
        下载 开发文档
      </el-button>
    </div>
    
    <p class="intro">
      OIDC (OpenID Connect) 是基于 OAuth 2.0 的现代身份认证协议，本平台基于 <strong>Ory Hydra</strong> 实现了完整的 OIDC 标准流程。
      适用于需要标准 OIDC 协议支持、Token 管理、长期会话等场景。
    </p>

    <div class="section">
      <h3>1. 概述</h3>
      
      <div class="feature-card">
        <h4>✅ 适用场景</h4>
        <ul>
          <li>需要标准 OIDC 协议支持的应用</li>
          <li>需要获取 ID Token 和 Access Token 的场景</li>
          <li>需要支持 Refresh Token 的长期会话</li>
          <li>需要标准化的用户信息获取（UserInfo 端点）</li>
          <li>需要与第三方 OIDC 库/框架集成</li>
        </ul>
      </div>

      <div class="feature-card">
        <h4>🔄 OIDC vs Simple API</h4>
        <el-table :data="comparisonTable" border style="width: 100%">
          <el-table-column prop="feature" label="特性" width="200" />
          <el-table-column prop="oidc" label="OIDC" />
          <el-table-column prop="simple" label="Simple API" />
        </el-table>
        <p class="intro" style="margin-top: 12px;">
          另：<strong>无（<code>NONE</code>）</strong>表示仅快捷导航、未对接 Ticket 与 OIDC；账号映射仍决定用户是否能在导航中看到该应用，跳转请使用 <code>POST /simple/sso-login</code>（返回导航 URL，无 ticket）。详见《平台账号管理接口文档》与《客户端接口 API 指南》第 8 节。
        </p>
      </div>
    </div>

    <div class="section">
      <h3>2. 核心流程</h3>
      <div class="flow-chart">
        <div class="step">
          <div class="self-action" style="margin-left: 0; width: auto; display: inline-block;">
            1. 应用重定向用户到授权端点
          </div>
          <div
            class="arrow client-to-uap"
            style="position: relative; top: 0; left: 10px; width: 120px;"
          ></div>
          <div class="self-action" style="margin-left: 10px; width: auto; display: inline-block;">
            2. 用户在 UAP 完成登录
          </div>
        </div>

        <div class="step" style="margin-top: 10px;">
          <div class="self-action" style="margin-left: 0; width: auto; display: inline-block;">
            3. 用户确认授权（自动）
          </div>
          <div
            class="arrow uap-to-client"
            style="position: relative; top: 0; left: 10px; width: 120px;"
          ></div>
          <div class="self-action" style="margin-left: 10px; width: auto; display: inline-block;">
            4. 平台返回授权码
          </div>
        </div>

        <div class="step" style="margin-top: 10px;">
          <div class="self-action" style="margin-left: 0; width: auto; display: inline-block;">
            5. 应用用授权码换取 Token
          </div>
          <div
            class="arrow client-to-user"
            style="position: relative; top: 0; left: 10px; width: 120px; border-style: dashed;"
          ></div>
          <div class="self-action" style="margin-left: 10px; width: auto; display: inline-block;">
            6. 使用 Token 获取用户信息
          </div>
        </div>
      </div>
    </div>

    <div class="section">
      <h3>3. 准备工作</h3>
      
      <h4>创建 OIDC 应用</h4>
      <ol>
        <li>登录 UAP 管理后台，进入「应用管理」</li>
        <li>点击「创建应用」，<strong>协议类型选择 <code>OIDC</code></strong></li>
        <li><strong>关键配置</strong>：
          <ul>
            <li><strong>回调地址 (Redirect URIs)</strong>：填写您的应用回调地址，例如：
              <code>http://your-app.com/callback</code>
            </li>
            <li><strong>支持的 Scopes</strong>：<code>openid offline profile</code></li>
          </ul>
        </li>
        <li>保存后获取 <code>App ID</code> 和 <code>App Secret</code></li>
      </ol>
      
      <el-alert title="重要提示" type="warning" :closable="false" show-icon style="margin-top: 15px;">
        <div>
          <strong>Redirect URIs</strong> 必须与您的应用实际回调地址完全匹配（包括协议、域名、端口、路径）。
        </div>
      </el-alert>
    </div>

    <div class="section">
      <h3>4. 前端集成</h3>
      
      <h4>启动授权流程</h4>
      <div class="code-block">
        <pre>
const clientId = 'YOUR_APP_ID'
const redirectUri = encodeURIComponent('http://your-app.com/callback')
const scope = 'openid offline profile'
const state = generateRandomString() // 用于防止 CSRF 攻击

const authUrl = `http://localhost/hydra/oauth2/auth?` +
  `client_id=${clientId}&` +
  `response_type=code&` +
  `scope=${scope}&` +
  `redirect_uri=${redirectUri}&` +
  `state=${state}`

window.location.href = authUrl
        </pre>
      </div>

      <h4>处理回调</h4>
      <p>用户完成授权后，平台会重定向回您的回调地址：</p>
      <code>http://your-app.com/callback?code=AUTHORIZATION_CODE&state=YOUR_STATE</code>
    </div>

    <div class="section">
      <h3>5. 后端集成（Token 交换）</h3>
      
      <h4>接口信息</h4>
      <ul>
        <li><strong>接口地址</strong>: <code>POST http://localhost/hydra/oauth2/token</code></li>
        <li><strong>Content-Type</strong>: <code>application/x-www-form-urlencoded</code></li>
      </ul>

      <h4>请求参数</h4>
      <el-table :data="tokenParams" border style="width: 100%">
        <el-table-column prop="param" label="参数" width="150" />
        <el-table-column prop="type" label="类型" width="100" />
        <el-table-column prop="required" label="必填" width="80" />
        <el-table-column prop="desc" label="说明" />
      </el-table>

      <el-tabs type="border-card" class="code-tabs" style="margin-top: 20px;">
        <el-tab-pane label="Python">
          <div class="code-block">
            <pre>
import requests

def exchange_code_for_token(code: str, redirect_uri: str):
    token_url = "http://localhost/hydra/oauth2/token"
    
    data = {
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": redirect_uri,
        "client_id": "YOUR_APP_ID",
        "client_secret": "YOUR_APP_SECRET"
    }
    
    response = requests.post(token_url, data=data)
    if response.status_code == 200:
        token_data = response.json()
        return {
            "access_token": token_data["access_token"],
            "id_token": token_data["id_token"],
            "refresh_token": token_data.get("refresh_token"),
            "expires_in": token_data["expires_in"]
        }
    else:
        raise Exception(f"Token exchange failed: {response.text}")
            </pre>
          </div>
        </el-tab-pane>

        <el-tab-pane label="Node.js">
          <div class="code-block">
            <pre>
const axios = require('axios')

async function exchangeCodeForToken(code, redirectUri) {
  const tokenUrl = 'http://localhost/hydra/oauth2/token'
  
  const params = new URLSearchParams()
  params.append('grant_type', 'authorization_code')
  params.append('code', code)
  params.append('redirect_uri', redirectUri)
  params.append('client_id', 'YOUR_APP_ID')
  params.append('client_secret', 'YOUR_APP_SECRET')
  
  const response = await axios.post(tokenUrl, params, {
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  })
  
  return {
    access_token: response.data.access_token,
    id_token: response.data.id_token,
    refresh_token: response.data.refresh_token,
    expires_in: response.data.expires_in
  }
}
            </pre>
          </div>
        </el-tab-pane>

        <el-tab-pane label="Java">
          <div class="code-block">
            <pre>
import org.springframework.web.client.RestTemplate;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

public TokenResponse exchangeCodeForToken(String code, String redirectUri) {
    RestTemplate restTemplate = new RestTemplate();
    
    MultiValueMap&lt;String, String&gt; params = new LinkedMultiValueMap&lt;&gt;();
    params.add("grant_type", "authorization_code");
    params.add("code", code);
    params.add("redirect_uri", redirectUri);
    params.add("client_id", "YOUR_APP_ID");
    params.add("client_secret", "YOUR_APP_SECRET");
    
    return restTemplate.postForObject(
        "http://localhost/hydra/oauth2/token",
        params,
        TokenResponse.class
    );
}
            </pre>
          </div>
        </el-tab-pane>
      </el-tabs>
    </div>

    <div class="section">
      <h3>6. 获取用户信息</h3>
      
      <h4>从 ID Token 获取（推荐）</h4>
      <p>ID Token 是 JWT 格式，可以直接解析：</p>
      <div class="code-block">
        <pre>
function parseJwt(token) {
  const base64Url = token.split('.')[1]
  const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
  const jsonPayload = decodeURIComponent(
    atob(base64)
      .split('')
      .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
      .join('')
  )
  return JSON.parse(jsonPayload)
}

const claims = parseJwt(idToken)
console.log('User ID:', claims.sub)
console.log('Email:', claims.email)
console.log('Preferred Username:', claims.preferred_username)
        </pre>
      </div>

      <h4>ID Token Claims</h4>
      <el-table :data="idTokenClaims" border style="width: 100%">
        <el-table-column prop="claim" label="Claim" width="200" />
        <el-table-column prop="desc" label="说明" />
        <el-table-column prop="example" label="示例" />
      </el-table>
    </div>

    <div class="section">
      <h3>7. Refresh Token 使用</h3>
      <p>如果请求了 <code>offline</code> scope，可以使用 Refresh Token 刷新 Access Token：</p>
      <div class="code-block">
        <pre>
def refresh_access_token(refresh_token: str):
    token_url = "http://localhost/hydra/oauth2/token"
    
    data = {
        "grant_type": "refresh_token",
        "refresh_token": refresh_token,
        "client_id": "YOUR_APP_ID",
        "client_secret": "YOUR_APP_SECRET"
    }
    
    response = requests.post(token_url, data=data)
    return response.json()
        </pre>
      </div>
    </div>

    <div class="section">
      <h3>8. 标准端点</h3>
      <ul>
        <li><strong>授权端点</strong>: <code>http://localhost/hydra/oauth2/auth</code></li>
        <li><strong>Token 端点</strong>: <code>http://localhost/hydra/oauth2/token</code></li>
        <li><strong>UserInfo 端点</strong>: <code>http://localhost/hydra/userinfo</code></li>
        <li><strong>发现端点</strong>: <code>http://localhost/hydra/.well-known/openid-configuration</code></li>
      </ul>
      <el-alert title="注意" type="info" :closable="false" show-icon style="margin-top: 15px;">
        本地开发默认通过 <code>http://localhost/hydra/...</code> 访问 Hydra，
        生产环境请将 <code>http://localhost</code> 替换为实际的前端网关域名（例如 <code>https://sso.example.com</code>）。
      </el-alert>
    </div>

    <div class="section">
      <h3>9. 常见问题</h3>
      <el-collapse>
        <el-collapse-item title="授权码有效期是多久？" name="1">
          <p>授权码通常有效期为 10 分钟，建议立即交换 Token。</p>
        </el-collapse-item>
        <el-collapse-item title="如何判断用户是否已登录？" name="2">
          <p>检查是否有有效的 Access Token，或调用 UserInfo 端点验证。</p>
        </el-collapse-item>
        <el-collapse-item title="Refresh Token 会过期吗？" name="3">
          <p>Refresh Token 通常长期有效，但可能被撤销。建议在刷新失败时引导用户重新登录。</p>
        </el-collapse-item>
        <el-collapse-item title="ID Token 和 Access Token 的区别？" name="4">
          <p><strong>ID Token</strong>: 包含用户身份信息，用于标识用户。<br>
          <strong>Access Token</strong>: 用于访问受保护的资源（如 UserInfo 端点）。</p>
        </el-collapse-item>
      </el-collapse>
    </div>

    <div class="section">
      <h3>10. 安全建议</h3>
      <ul>
        <li>✅ 生产环境必须使用 HTTPS</li>
        <li>✅ 始终验证授权回调中的 <code>state</code> 参数</li>
        <li>✅ 验证 ID Token 的签名和过期时间</li>
        <li>✅ App Secret 必须安全存储，不要暴露在前端代码中</li>
        <li>✅ Access Token 和 Refresh Token 应安全存储（如 httpOnly Cookie）</li>
      </ul>
    </div>
  </div>
</template>

<script setup lang="ts">
import { Download } from '@element-plus/icons-vue'
import { useHelpDocs } from '../../composables/useHelpDocs'

const { downloadDoc } = useHelpDocs()

const comparisonTable = [
  {
    feature: '协议标准',
    oidc: 'OAuth 2.0 / OIDC',
    simple: '自定义 Ticket 机制'
  },
  {
    feature: 'Token 类型',
    oidc: 'ID Token, Access Token, Refresh Token',
    simple: 'Ticket (一次性)'
  },
  {
    feature: '用户信息获取',
    oidc: 'UserInfo 端点',
    simple: '验证接口返回'
  },
  {
    feature: '会话管理',
    oidc: 'Refresh Token',
    simple: '需重新登录'
  },
  {
    feature: '适用场景',
    oidc: '现代应用、第三方集成',
    simple: '传统系统、快速对接'
  }
]

const tokenParams = [
  {
    param: 'grant_type',
    type: 'string',
    required: '是',
    desc: '固定值: authorization_code'
  },
  {
    param: 'code',
    type: 'string',
    required: '是',
    desc: '从回调 URL 获取的授权码'
  },
  {
    param: 'redirect_uri',
    type: 'string',
    required: '是',
    desc: '必须与授权请求中的 redirect_uri 完全一致'
  },
  {
    param: 'client_id',
    type: 'string',
    required: '是',
    desc: '您的 App ID'
  },
  {
    param: 'client_secret',
    type: 'string',
    required: '是',
    desc: '您的 App Secret'
  }
]

const idTokenClaims = [
  {
    claim: 'sub',
    desc: '用户唯一标识（User ID）',
    example: '"123"'
  },
  {
    claim: 'preferred_username',
    desc: '映射的用户名',
    example: '"user_zhangsan"'
  },
  {
    claim: 'email',
    desc: '映射的邮箱',
    example: '"zhangsan@example.com"'
  },
  {
    claim: 'phone_number',
    desc: '用户手机号',
    example: '"13800138000"'
  },
  {
    claim: 'exp',
    desc: 'Token 过期时间（Unix 时间戳）',
    example: '1704067200'
  },
  {
    claim: 'iat',
    desc: 'Token 签发时间',
    example: '1704063600'
  }
]
</script>

<style scoped>
@import './help.css';
</style>
