openapi: "3.0.3"
info:
  title: "Common Auth External API"
  version: "1.0.0"
  description: "对外开放接口契约（IdP OAuth2 + 上游 OAuth）。

    本规范为唯一外部 API 契约源，由构建脚本生成子集与 AI 文档。

    版本策略：语义化版本；破坏性变更仅在主版本升级时发布。"
servers:
  - url: "/"
tags:
  - name: "OAuth2_RP"
    description: "第三方应用接入统一认证（授权码模式）"
  - name: "OAuth_Upstream"
    description: "统一认证站接入上游 OAuth 提供方"
  - name: "Auth"
    description: "对外 OAuth 场景下的用户信息读取"
components:
  securitySchemes:
    bearerAuth:
      type: "http"
      scheme: "bearer"
      bearerFormat: "JWT"
    cookieAuth:
      type: "apiKey"
      in: "cookie"
      name: "auth_token"
    clientBasic:
      type: "http"
      scheme: "basic"
      description: "client_id:client_secret"
  schemas:
    ApiErrorBody:
      type: "object"
      required:
        - "error"
      properties:
        error:
          type: "object"
          required:
            - "code"
            - "message"
          properties:
            code:
              type: "string"
            message:
              type: "string"
            details:
              type: "object"
              additionalProperties: true
    OAuth2TokenSuccess:
      type: "object"
      required:
        - "access_token"
        - "token_type"
        - "expires_in"
        - "scope"
      properties:
        access_token:
          type: "string"
        token_type:
          type: "string"
          example: "Bearer"
        expires_in:
          type: "integer"
          example: 2592000
        scope:
          type: "string"
          example: "openid profile email"
    CurrentUserProfile:
      type: "object"
      properties:
        id:
          type: "string"
        uni_id:
          type: "string"
          nullable: true
        email:
          type: "string"
          nullable: true
        name:
          type: "string"
          nullable: true
        avatar:
          type: "string"
          nullable: true
        emailVerified:
          type: "boolean"
        phone:
          type: "string"
          nullable: true
        phoneVerified:
          type: "boolean"
        mfaEnabled:
          type: "boolean"
        loginEmailCodeEnabled:
          type: "boolean"
        locale:
          type: "string"
          nullable: true
        timezone:
          type: "string"
          nullable: true
        status:
          type: "string"
          nullable: true
        createdAt:
          type: "string"
          format: "date-time"
          nullable: true
paths:
  /api/oauth/authorize:
    get:
      tags:
        - "OAuth2_RP"
      summary: "打开授权页"
      parameters:
        - name: "client_id"
          in: "query"
          required: true
          schema:
            type: "string"
        - name: "redirect_uri"
          in: "query"
          required: true
          schema:
            type: "string"
        - name: "response_type"
          in: "query"
          required: true
          schema:
            type: "string"
            enum:
              - "code"
        - name: "scope"
          in: "query"
          schema:
            type: "string"
        - name: "state"
          in: "query"
          schema:
            type: "string"
        - name: "code_challenge"
          in: "query"
          schema:
            type: "string"
        - name: "code_challenge_method"
          in: "query"
          schema:
            type: "string"
            enum:
              - "S256"
              - "plain"
      responses:
        "302":
          description: "重定向到登录页、授权确认页或错误页"
  /api/oauth/authorize/confirm:
    get:
      tags:
        - "OAuth2_RP"
      summary: "授权确认并签发授权码"
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: "client_id"
          in: "query"
          required: true
          schema:
            type: "string"
        - name: "redirect_uri"
          in: "query"
          required: true
          schema:
            type: "string"
        - name: "response_type"
          in: "query"
          required: true
          schema:
            type: "string"
            enum:
              - "code"
        - name: "scope"
          in: "query"
          schema:
            type: "string"
        - name: "state"
          in: "query"
          schema:
            type: "string"
        - name: "code_challenge"
          in: "query"
          schema:
            type: "string"
        - name: "code_challenge_method"
          in: "query"
          schema:
            type: "string"
            enum:
              - "S256"
              - "plain"
      responses:
        "302":
          description: "回跳客户端 redirect_uri，并附带 code/state"
        "400": &a1
          description: "错误 JSON"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiErrorBody"
        "401": *a1
  /api/oauth/authorize/cancel:
    get:
      tags:
        - "OAuth2_RP"
      summary: "取消授权"
      description: "回跳客户端 redirect_uri，附带 error=access_denied（及可选 state）。"
      parameters:
        - name: "client_id"
          in: "query"
          required: true
          schema:
            type: "string"
        - name: "redirect_uri"
          in: "query"
          required: true
          schema:
            type: "string"
        - name: "state"
          in: "query"
          schema:
            type: "string"
      responses:
        "302":
          description: "回跳客户端 redirect_uri?error=access_denied"
  /api/oauth/token:
    post:
      tags:
        - "OAuth2_RP"
      summary: "授权码换票"
      description: "支持 form-urlencoded 与 JSON 请求体；client_id/client_secret 可通过 body、query、自定义 header 或 Basic Auth 传递。"
      security:
        - clientBasic: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: "object"
              required:
                - "grant_type"
                - "code"
                - "redirect_uri"
              properties:
                grant_type:
                  type: "string"
                  enum:
                    - "authorization_code"
                code:
                  type: "string"
                client_id:
                  type: "string"
                client_secret:
                  type: "string"
                redirect_uri:
                  type: "string"
                code_verifier:
                  type: "string"
          application/json:
            schema:
              type: "object"
              required:
                - "grant_type"
                - "code"
                - "redirect_uri"
              properties:
                grant_type:
                  type: "string"
                  enum:
                    - "authorization_code"
                code:
                  type: "string"
                client_id:
                  type: "string"
                client_secret:
                  type: "string"
                redirect_uri:
                  type: "string"
                code_verifier:
                  type: "string"
      responses:
        "200":
          description: "成功"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OAuth2TokenSuccess"
        "400": *a1
        "401": *a1
  /api/auth/me:
    get:
      tags:
        - "Auth"
      summary: "读取当前用户资料"
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: "成功"
          content:
            application/json:
              schema:
                type: "object"
                properties:
                  data:
                    $ref: "#/components/schemas/CurrentUserProfile"
        "401": *a1
  /api/oauth/email/send:
    post:
      tags:
        - "OAuth2_RP"
        - "OAuth_Upstream"
      summary: "发送补绑邮箱验证码"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              required:
                - "email"
              properties:
                email:
                  type: "string"
                  format: "email"
      responses:
        "200":
          description: "已发送"
        "400": *a1
        "401": *a1
        "403": *a1
        "429": *a1
        "500": *a1
  /api/oauth/email/verify:
    post:
      tags:
        - "OAuth2_RP"
        - "OAuth_Upstream"
      summary: "验证补绑邮箱验证码"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: "object"
              required:
                - "code"
              properties:
                code:
                  type: "string"
                deviceFingerprint:
                  type: "string"
                  maxLength: 255
      responses:
        "200":
          description: "绑定成功并更新会话"
        "400": *a1
        "401": *a1
        "429": *a1
  /api/oauth/providers:
    get:
      tags:
        - "OAuth_Upstream"
      summary: "已配置的上游提供方列表"
      responses:
        "200":
          description: "成功"
          content:
            application/json:
              schema:
                type: "object"
                properties:
                  data:
                    type: "array"
                    items:
                      type: "string"
  /api/oauth/{provider}:
    get:
      tags:
        - "OAuth_Upstream"
      summary: "发起上游 OAuth 登录"
      parameters:
        - name: "provider"
          in: "path"
          required: true
          schema:
            type: "string"
      responses:
        "302":
          description: "重定向至第三方授权页"
        "400": *a1
        "503": *a1
  /api/oauth/callback/{provider}:
    get:
      tags:
        - "OAuth_Upstream"
      summary: "处理上游 OAuth 回调"
      parameters:
        - name: "provider"
          in: "path"
          required: true
          schema:
            type: "string"
      responses:
        "302":
          description: "重定向至站内页面或错误页"
  /api/oauth/bind/{provider}:
    get:
      tags:
        - "OAuth_Upstream"
      summary: "发起上游 OAuth 账号绑定"
      security:
        - cookieAuth: []
        - bearerAuth: []
      parameters:
        - name: "provider"
          in: "path"
          required: true
          schema:
            type: "string"
      responses:
        "302":
          description: "重定向至第三方授权页"
        "400": *a1
        "503": *a1
