# 使用 Vimine 登入

> 從點選到回呼的 OAuth 2.1 + PKCE 流程、工作階段、API 呼叫和登出，使用或不使用 SDK。

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

協會的會員使用自己的 Vimine 帳戶登入您的網站，並透過明確的同意，授權您使用他們的部分資料。這就是 OAuth 2.1 加 PKCE：密碼只在 Vimine 的頁面上輸入，絕不會在您的頁面上輸入；您的網站收到的權杖僅限於已授予的權限。

:::note[開始之前]
您需要一個**網站**或**原生應用**用戶端，並已登記您的 `redirect_uri`——參見[登記用戶端](https://developers.vimine.network/zh-hant/start/register-a-client/)——還需要安裝 SDK（[安裝 SDK](https://developers.vimine.network/zh-hant/start/install/)）。如需在不觸及真實資料的情況下開發，請使用**測試**用戶端和[沙箱](https://developers.vimine.network/zh-hant/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-hant/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();                            // 會員卡權杖（QR Code）及其有效時段
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-hant/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-hant/sdk/browser/)；回應結構見[型別](https://developers.vimine.network/zh-hant/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": "…" }`。
