# 使用 Vimine 登录

> 从点击到回调的 OAuth 2.1 + PKCE 流程、会话、API 调用和退出登录，使用或不使用 SDK。

URL: https://developers.vimine.network/zh-hans/guides/sign-in/

协会的会员使用自己的 Vimine 账户登录您的网站，并通过明确的同意，授权您使用他们的部分数据。这就是 OAuth 2.1 加 PKCE：密码只在 Vimine 的页面上输入，绝不会在您的页面上输入；您的网站收到的令牌仅限于已授予的权限。

:::note[开始之前]
您需要一个**网站**或**原生应用**客户端，并已登记您的 `redirect_uri`——参见[登记客户端](https://developers.vimine.network/zh-hans/start/register-a-client/)——还需要安装 SDK（[安装 SDK](https://developers.vimine.network/zh-hans/start/install/)）。如需在不触及真实数据的情况下开发，请使用**测试**客户端和[沙盒](https://developers.vimine.network/zh-hans/guides/sandbox/)。
:::

## 流程

1. 会员**点击**后，您的网站把浏览器（顶层窗口，绝不能是 iframe）送到授权页面：
   ```
   https://app.vimine.network/authorize
       ?client_id=vmc_…&redirect_uri=https://www.example.org/vimine/callback
       &response_type=code&state=…&code_challenge=…&code_challenge_method=S256
       &scope=profile:read%20membership:read
   ```
2. 会员登录（如果尚未登录），并看到**谁**将获得**什么**权限。会员确认或取消。
3. Vimine 把浏览器送回您的 `redirect_uri`，并带上 `code` 和 `state`（或 `error=access_denied`）。
4. 您的网站在 `POST https://api.vimine.network/v1/oauth/token` 交换授权码（`grant_type=authorization_code`、`code`、`code_verifier`、`client_id`、`redirect_uri`），获得一个访问令牌（10 分钟）和一个刷新令牌（轮换；30 天，闲置 14 天即失效）。

SDK 会为您完成以上全部步骤。

## 使用 SDK

```js title="任意页面"
import { Vimine } from '@vimine/sdk';

const vimine = new Vimine({
    clientId: 'vmc_…',
    redirectUri: 'https://www.example.org/vimine/callback',
    scopes: ['profile:read', 'membership:read']
});

// 由点击触发：`returnTo` 是您自己的状态，回调之后会原样交还给您
document.querySelector('#sign-in').addEventListener('click', () => vimine.signIn({ returnTo: location.pathname }));
```

```js title="回调页面"
const result = await vimine.handleRedirect();   // URL 中没有登录参数时返回 null
if (result) location.replace(result.returnTo || '/');
```

`signIn()` 的选项：

| 选项 | 作用 |
|---|---|
| `scopes` | 本次登录要申请的权限（默认：构造函数中的权限，或 `profile:read membership:read`） |
| `prompt: 'login'` | 即使存在有效的 Vimine 会话，也强制显示登录表单 |
| `prompt: 'consent'` | 始终显示授权同意页面 |
| `returnTo` | 您自己的状态（例如会员开始登录时所在的页面），保存在本地，由 `handleRedirect()` 返回 |

会员取消时，`handleRedirect()` 抛出 `code` 为 `access_denied` 的 [`VimineError`](https://developers.vimine.network/zh-hans/sdk/errors/)；当返回与本浏览器发起的登录不匹配时，抛出 `state_mismatch` 或 `no_flow`。

## 会话

- 令牌保存在 `sessionStorage` 中（每个标签页各自独立）。`storage: 'local'` 让多个标签页共享令牌并同步退出登录；`'memory'` 则从不持久保存。您也可以传入自己的适配器。
- `vimine.getAccessToken()` 会在令牌到期前 30 秒自动刷新；收到 401 的调用会在刷新后重试一次。**重复使用**的刷新令牌（例如被复制的令牌）会撤销整个会话家族。
- `vimine.onChange(session => …)` 会通知登录、刷新和退出登录。
- `vimine.getSession().sub` 是会员**在您的协会中**的标识（`vms_…`）：长期稳定，在每个协会中都不同，绝不是内部 id。

## 您可以请求的内容

```js
await vimine.me.get();                                     // profile:read
await vimine.me.update({ phone: '+39 …' });                // profile:write
await vimine.membership.get();                             // membership:read — status, verdict, expires_at, statuto, card
await vimine.membership.events({ limit: 20 });             // 状态历史
await vimine.membership.statuto();                         // 现行章程，以及会员是否已接受
await vimine.membership.acceptStatuto({ version: 3 });     // statuto:accept
await vimine.membership.request({ accept_statuto: { version: 3 }, privacy_notice: { version: 2 } });   // membership:request → 待处理的会员卡
await vimine.membership.card();                            // 会员卡令牌（二维码）及其时间窗口
await vimine.legal.acceptances();                          // legal:read → 已接受的内容和尚待确认的内容
await vimine.legal.record({ kind: 'privacy_notice', version: 2 });   // legal:accept → 确认已阅读协会的隐私声明
await vimine.connections.list();  await vimine.connections.revoke('vmc_…');
```
:::note[法律文件]
`legal.acceptances()` 列出此人已接受的文件，并在 `pending` 下列出现行版本中尚待确认的文件：账户使用条款和平台的隐私声明（`platform`），以及本协会面向会员的隐私声明（`privacy_notice`）。请展示文本（`GET /v1/legal/{kind}`、`GET /v1/aps/{slug}/privacy-notice`），并用 `legal.record()` 记录确认：版本必须是现行版本，否则返回 `422 <kind>_outdated`。
:::
:::note[生效]
一份申请（`membership.request()`）之后会怎样，取决于**协会如何接纳会员**——即公开资料中的 `admission`，详见[入会审批与生效](https://developers.vimine.network/zh-hans/guides/admission/)。如果由理事会决定，申请保持 `pending`，并带有按顺序排列的 `pending_reasons`（先 `approval`，再 `payment`），会费只在获准入会之后缴纳（`payment_open`）；否则，免费类型的会员资格一创建即为**有效**，收费类型则在记录会费后生效。有效的会员资格可能带有一个未来的 `valid_from`：从那时起，会员卡在入场时才算数。只要出现 `payment`，就请向会员展示 `quota` 和 `payment_instructions`。
:::

`verdict` 是入场时真正要看的结果：`valid`、`expired`、`statuto_required`（重新接受章程已逾期）或 `inactive`。尚未入会的人也可以登录并使用 `membership.request`：非会员同样可以同意入会相关的权限；其他权限则要求在该协会持有会员卡。

每个方法的详细说明见 [`Vimine`](https://developers.vimine.network/zh-hans/sdk/browser/)；响应结构见[类型](https://developers.vimine.network/zh-hans/sdk/types/)。

## 退出登录

```js
await vimine.signOut();                        // 撤销 API 会话并将其清除
await vimine.signOut({ everywhere: true });    // 同时结束会员的 Vimine 会话，然后回到您的网站
```

使用 `everywhere` 时，浏览器会经过 `https://app.vimine.network/oauth/logout?client_id=…&post_logout_redirect_uri=…`；返回地址必须位于为该客户端登记的来源上。当会员退出 Vimine 时，其所有应用的 API 会话都会被撤销（单点退出）；当会员在**已连接的应用**中撤销某个应用或重置密码时，也是如此。

## 不使用 SDK

如果您无法使用 SDK（其他编程语言、低代码平台），该流程就是标准的 OAuth 2.1。

**授权**——`GET https://app.vimine.network/authorize`

| 参数 | |
|---|---|
| `client_id` | 您的 `vmc_…` |
| `redirect_uri` | 已登记的地址之一，必须完全一致 |
| `response_type` | `code` |
| `state` | 随机值，返回时用于比对 |
| `code_challenge`、`code_challenge_method` | PKCE，仅支持 `S256` |
| `scope` | 以空格分隔的权限 |
| `prompt` | 可选：`login`、`consent` |
| `ui_locales` | 可选：Vimine 支持的语言之一——`it`（默认）、`en`、`zh-Hans`、`zh-Hant`、`fr`、`es`、`de`、`pt-BR` |

**交换**——`POST https://api.vimine.network/v1/oauth/token`，请求体为 `application/x-www-form-urlencoded`：

```
grant_type=authorization_code&code=…&code_verifier=…&client_id=vmc_…&redirect_uri=…
```

```json title="响应"
{ "access_token": "…", "token_type": "Bearer", "expires_in": 600, "refresh_token": "…",
  "scope": "profile:read membership:read", "sub": "vms_…" }
```

**刷新**——同一端点，使用 `grant_type=refresh_token&refresh_token=…&client_id=…`：响应中带有一个**新的**刷新令牌（旧令牌为并发请求再保持有效 15 秒，此后再使用它会撤销整个会话家族）。

**撤销**——`POST /v1/oauth/revoke`，参数为 `token=…&token_type_hint=refresh_token`（RFC 7009）。

**发现**——`GET https://api.vimine.network/.well-known/oauth-authorization-server`（RFC 8414）列出端点、scope 和支持的方法。

这些端点的错误采用 OAuth 格式：`{ "error": "invalid_grant", "error_description": "…" }`。
