# nestjs-authnpaas

`nestjs-authnpaas` 是一个轻量级的 NestJS 模块，用于处理身份验证检查。它提供了一个全局守卫，以确保用户在访问端点之前已经登录。

## 目录

- [功能](#功能)
- [安装](#安装)
- [快速开始](#快速开始)
- [装饰器](#装饰器)
  - [@NeedLogin()](#needlogin)
- [服务](#服务)
  - [AuthNPaasService](#authnpaasservice)
- [工作原理](#工作原理)

## 功能

- **身份验证检查**: 确保用户在访问受保护端点之前已经登录。
- **登录重定向**: 自动将未经身份验证的用户重定向到登录 URL。
- **用户 ID 转换**: 支持妙搭用户 ID 与飞书用户 ID 互转。

## 安装

```bash
npm install @lark-apaas/nestjs-authnpaas
# 或者
yarn add @lark-apaas/nestjs-authnpaas
```

## 快速开始

在您的根模块 `app.module.ts` 中导入并配置 `AuthNPaasModule`。

```typescript
import { Module } from '@nestjs/common';
import { AuthNPaasModule } from '@lark-apaas/nestjs-authnpaas';

@Module({
  imports: [
    AuthNPaasModule.forRoot(),
  ],
  controllers: [/* ... */],
  providers: [/* ... */],
})
export class AppModule {}
```


## 装饰器

### @NeedLogin()

使用 `@NeedLogin()` 装饰器在特定端点上强制执行身份验证。

#### 控制器级别

```typescript
import { Controller, Get } from '@nestjs/common';
import { NeedLogin } from '@lark-apaas/nestjs-authnpaas';

@Controller('dashboard')
@NeedLogin() // 此控制器中的所有路由都需要登录
export class DashboardController {
  @Get()
  getDashboard() {
    return '您的个人仪表板';
  }
}
```

#### 方法级别

```typescript
import { Controller, Get } from '@nestjs/common';
import { NeedLogin } from '@lark-apaas/nestjs-authnpaas';

@Controller('api')
export class ApiController {
  @Get('public-data')
  getPublicData() {
    // 此端点不需要身份验证
    return '一些公共数据';
  }

  @Get('private-data')
  @NeedLogin() // 此端点需要身份验证
  getPrivateData() {
    return '一些秘密数据';
  }
}
```

## 服务

### AuthNPaasService

`AuthNPaasService` 提供妙搭用户 ID 与飞书用户 ID 的转换能力。模块注册后即可通过依赖注入使用。

涉及的几种用户 ID（不要混用）：

| ID | 说明 | 示例 |
|---|---|---|
| 妙搭用户 ID（ForceUserID） | 妙搭侧用户 ID，全局唯一 | `1784973978546236` |
| 飞书 user_id（Employee ID） | 租户内用户 ID，可自定义，即飞书开放平台的 `user_id` | `18b85ad9` |
| LarkID | 飞书全局用户 ID，代表用户的物理身份 | `7311528002225963027` |
| open_id | 飞书应用内用户 ID，同一用户在不同飞书应用下不同 | `ou_7dab8a3d3cdcc9da365777c7ad535d62` |
| union_id | 同一用户在同一应用开发商下的多个飞书应用间相同 | `on_cad4860e7af114fb4ff6c5d496d1dd76` |

| 转换方向 | 方法 |
|---|---|
| 当前登录用户 → 飞书 user_id | `getCurrentUserLarkUserId()` |
| 妙搭用户 ID → 飞书 user_id | `getBatchLarkUserIds(userIds)` |
| 飞书 user_id → 妙搭用户 ID | `getBatchMiaodaUserIds(employeeIds)` |
| 妙搭用户 ID → LarkID | `getBatchLarkIds(userIds)` |
| LarkID → 妙搭用户 ID | `getBatchMiaodaUserIdsByLarkIds(larkIds)` |
| 妙搭用户 ID → open_id / union_id | `getBatchOpenIds(userIds, options?)` / `getBatchUnionIds(userIds, options?)` |
| open_id / union_id → 妙搭用户 ID | `getBatchMiaodaUserIdsByOpenIds(openIds, options?)` / `getBatchMiaodaUserIdsByUnionIds(unionIds, options?)` |

open_id / union_id 与飞书应用绑定：`options.feishuClientId` 传飞书应用 App ID（`cli_` 开头），结果 / 入参按该应用视角解释；不传时平台兜底用妙搭应用。

> 方法名 `getCurrentUserLarkUserId` / `getBatchLarkUserIds` 里的 LarkUserId 是历史命名，指的是飞书 user_id（Employee ID），不是 LarkID。

批量方法单次最多 100 个，返回数组与入参等长同序，转换失败的位置为 `null`。

```typescript
import { Injectable } from '@nestjs/common';
import { AuthNPaasService } from '@lark-apaas/nestjs-authnpaas';

@Injectable()
export class MyService {
  constructor(private readonly authn: AuthNPaasService) {}

  async example() {
    // 获取当前登录用户的飞书 user_id
    const larkUserId = await this.authn.getCurrentUserLarkUserId();
    // => 'xxx' | null

    // 批量转换妙搭 userId → 飞书 user_id（最多 100 个）
    const larkUserIds = await this.authn.getBatchLarkUserIds(['uid1', 'uid2']);
    // => ['18b85ad9', '2a3c4d5e']  顺序与输入对应，找不到的为 null

    // 批量转换 LarkID → 妙搭 userId（最多 100 个）
    const miaodaUserIds = await this.authn.getBatchMiaodaUserIdsByLarkIds(['7311528002225963027']);
    // => ['1784973978546236']
  }
}
```

#### getCurrentUserLarkUserId

从请求上下文获取当前妙搭 userId，调用平台接口转换为飞书 user_id。

- **返回值**: `Promise<string | null>` — 飞书 user_id，未登录或转换失败返回 `null`

#### getBatchLarkUserIds

批量将妙搭 userId 转换为飞书 user_id。

- **参数**: `userIds: string[]` — 妙搭用户 ID 列表，最多 100 个
- **返回值**: `Promise<(string | null)[]>` — 飞书 user_id 数组，与输入顺序一一对应，转换失败的项为 `null`
- **异常**: 当 `userIds` 超过 100 个时抛出错误

#### getBatchMiaodaUserIds

批量将飞书 user_id（Employee ID）转换为妙搭 userId。

- **参数**: `employeeIds: string[]` — 飞书 user_id 列表，最多 100 个
- **返回值**: `Promise<(string | null)[]>` — 妙搭 userId 数组，与输入顺序一一对应，转换失败的项为 `null`
- **异常**: 当 `employeeIds` 超过 100 个时抛出错误

#### getBatchLarkIds

批量将妙搭 userId 转换为 LarkID（飞书全局用户 ID）。

- **参数**: `userIds: string[]` — 妙搭用户 ID 列表，最多 100 个
- **返回值**: `Promise<(string | null)[]>` — LarkID 数组，与输入顺序一一对应，转换失败的项为 `null`
- **异常**: 当 `userIds` 超过 100 个时抛出错误

#### getBatchMiaodaUserIdsByLarkIds

批量将 LarkID（飞书全局用户 ID）转换为妙搭 userId。

- **参数**: `larkIds: string[]` — LarkID 列表，最多 100 个
- **返回值**: `Promise<(string | null)[]>` — 妙搭 userId 数组，与输入顺序一一对应，转换失败的项为 `null`
- **异常**: 当 `larkIds` 超过 100 个时抛出错误

#### getBatchOpenIds / getBatchUnionIds

批量将妙搭 userId 转换为飞书 open_id / union_id。

- **参数**: `userIds: string[]` — 妙搭用户 ID 列表，最多 100 个；`options?.feishuClientId` — 飞书应用 App ID（`cli_` 开头），不传兜底妙搭应用
- **返回值**: `Promise<(string | null)[]>` — 与输入顺序一一对应，转换失败的项为 `null`
- **异常**: 当 `userIds` 超过 100 个时抛出错误

#### getBatchMiaodaUserIdsByOpenIds / getBatchMiaodaUserIdsByUnionIds

批量将飞书 open_id / union_id 转换为妙搭 userId。

- **参数**: `openIds` / `unionIds: string[]` — 最多 100 个；`options?.feishuClientId` — ID 所属飞书应用的 App ID，不传兜底妙搭应用（其他应用签发的 open_id 会转换失败）
- **返回值**: `Promise<(string | null)[]>` — 与输入顺序一一对应，转换失败的项为 `null`
- **异常**: 当入参超过 100 个时抛出错误

## 工作原理

### AuthNPaasGuard

此模块将 `AuthNPaasGuard` 注册为全局守卫。在每个传入的请求上，守卫会执行以下步骤：

1. **检查 `@NeedLogin()`**: 它首先检查端点是否需要登录。
2. **确认身份验证状态**: 如果端点需要登录，守卫会确认 `request.userContext` 对象上的 `userId` 是否存在。
3. **如果未通过身份验证则重定向**: 如果端点标有 `@NeedLogin()` 且缺少 `userId`，它会将用户重定向到 `request.userContext` 上的 `loginUrl`。

