npm package: @scorehub/auth-sdk
Description: Auth-Service SDK for Node.js applications
Latest version: 1.5.0

# @scorehub/auth-sdk 使用文档

## 安装

```bash
npm install @scorehub/auth-sdk
```

---

## 初始化

```typescript
import { AuthClient } from '@scorehub/auth-sdk';

const authClient = new AuthClient({
  serviceUrl: 'https://api-test.scorehub.cn/auth/api/v1', // Auth-Service 地址
  clientId: 'your-client-id',                             // OAuth Client ID
  clientSecret: 'your-client-secret',                     // OAuth Client Secret
  publicKey: fs.readFileSync('./keys/public.pem'),         // RS256 公钥 (可选，用于本地验证)
  issuer: 'auth-service',                                 // JWT issuer，默认 'auth-service'
});
```

> `publicKey` 可选。提供时启用本地 JWT 验证（零网络延迟）；不提供时需调用 `introspectToken` 远程验证。

---

## 获取公钥

服务提供标准 JWKS 端点，可在运行时动态获取公钥。

**JWKS 地址：**
```
GET https://api-test.scorehub.cn/auth/api/v1/.well-known/jwks.json
```

### 动态获取并初始化（推荐）

```typescript
import { AuthClient } from '@scorehub/auth-sdk';

async function createAuthClient() {
  const res = await fetch('https://api-test.scorehub.cn/auth/api/v1/.well-known/jwks.json');
  const { keys } = await res.json();
  const x5c = keys[0].x5c[0];

  // 还原为 PEM 格式
  const publicKey = [
    '-----BEGIN PUBLIC KEY-----',
    ...x5c.match(/.{1,64}/g),
    '-----END PUBLIC KEY-----',
  ].join('\n');

  return new AuthClient({
    serviceUrl: 'https://api-test.scorehub.cn/auth/api/v1',
    clientId: 'your-client-id',
    clientSecret: 'your-client-secret',
    publicKey,
  });
}

const authClient = await createAuthClient();
```

### 提前下载保存为文件

```bash
curl -s https://api-test.scorehub.cn/auth/api/v1/.well-known/jwks.json \
  | node -e "
    const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
    const x5c = d.keys[0].x5c[0];
    const pem = '-----BEGIN PUBLIC KEY-----\n' + x5c.match(/.{1,64}/g).join('\n') + '\n-----END PUBLIC KEY-----';
    process.stdout.write(pem);
  " > public.pem
```

然后直接读取文件初始化：

```typescript
import fs from 'fs';

const authClient = new AuthClient({
  serviceUrl: 'https://api-test.scorehub.cn/auth/api/v1',
  clientId: 'your-client-id',
  clientSecret: 'your-client-secret',
  publicKey: fs.readFileSync('./public.pem'),
});
```

---

## Token 验证

### 本地验证（推荐，需配置 publicKey）

```typescript
try {
  const payload = await authClient.verifyAccessToken(token);
  console.log(payload.sub);      // 用户 ID
  console.log(payload.username); // 用户名
  console.log(payload.roles);    // 角色列表
  console.log(payload.scopes);   // 授权范围
} catch (err) {
  // TokenExpiredError / JsonWebTokenError
}
```

### 远程内省（无 publicKey 时使用）

```typescript
const result = await authClient.introspectToken(token);
if (result.active) {
  console.log(result.sub, result.username);
} else {
  // token 已失效
}
```

### TokenPayload 结构

| 字段 | 类型 | 说明 |
|------|------|------|
| `sub` | string | 用户/Client ID |
| `type` | `'user'` \| `'client'` | Token 类型 |
| `username` | string? | 用户名 |
| `roles` | string[]? | 角色列表 |
| `clientId` | string? | OAuth Client ID |
| `scopes` | string[] | 授权范围 |
| `jti` | string | JWT ID |
| `iss` | string | 签发者 |
| `iat` / `exp` | number | 签发/过期时间戳 |

---

## 用户认证

### 检查手机号是否已注册

```typescript
const { exists } = await authClient.checkUserExists('13800138000');
if (exists) {
  // 直接走登录流程
} else {
  // 引导注册
}
```

> 公开接口，无需鉴权。适合在注册/登录前预判手机号状态。

### 账号密码登录

```typescript
const result = await authClient.loginByPassword('username', 'password');
console.log(result.accessToken);
console.log(result.refreshToken);
console.log(result.user.id, result.user.roles);
```

### 短信验证码登录（含自动注册）

```typescript
// 先发送验证码
await authClient.sendSmsCode('13800138000');

// 验证码登录（手机号不存在时自动注册）
const result = await authClient.loginBySms('13800138000', '123456');
```

### 用户注册

```typescript
// 先发送验证码
await authClient.sendSmsCode('13800138000');

const result = await authClient.register({
  username: 'testuser',
  password: 'Password123',
  mobile: '13800138000',
  code: '123456',
  realname: '张三',  // 可选
});
```

### 重置密码

```typescript
await authClient.sendSmsCode('13800138000');
await authClient.resetPassword('13800138000', '123456', 'NewPassword123');
```

### 修改密码（已登录）

```typescript
await authClient.changePassword(
  accessToken,
  'OldPassword123',
  'NewPassword123',
);
```

> 该方法用于已登录用户使用旧密码修改为新密码，需要传入当前用户的 Access Token。

### 校验短信验证码（一次性）

```typescript
await authClient.verifySmsCode('13800138000', '123456');
// 成功则继续，失败抛出异常
```

---

## Token 管理

### 刷新 Token

```typescript
const tokenPair = await authClient.refreshToken(refreshToken);
// { access_token, refresh_token, token_type, expires_in }
```

### 吊销 Token

```typescript
await authClient.revokeToken(accessToken);
```

---

## 服务间调用

### 获取 Service Token（自动缓存）

```typescript
// client_credentials 方式，内置缓存，剩余有效期 < 60s 时自动刷新
const serviceToken = await authClient.getServiceToken(['read:users']);

fetch('http://other-service/api/data', {
  headers: { Authorization: `Bearer ${serviceToken}` },
});
```

### 根据 ID 查询用户信息

```typescript
const user = await authClient.getUserById('user-uuid');
// { sub, username, mobile, email, realname, nickname, avatar, roles }
```

---

## 管理端操作

> 使用 service token 鉴权，无需短信验证码，适合后台管理系统调用。

### 管理端创建用户

```typescript
const user = await authClient.adminCreateUser({
  username: 'newuser',
  password: 'Password123',
  mobile: '13900139000',
  realname: '李四',  // 可选
});
// { id, username, mobile }
```

### 管理端重置用户密码

```typescript
await authClient.adminResetPassword('user-uuid', 'NewPassword123');
```

### 设置用户状态

```typescript
// 将用户状态设为 DISABLED（禁用）
await authClient.setUserStatus('user-uuid', 'DISABLED');

// 将用户状态恢复为 ACTIVE（启用）
await authClient.setUserStatus('user-uuid', 'ACTIVE');

// 将用户状态设为 DELETED（软删除）
await authClient.setUserStatus('user-uuid', 'DELETED');
```

> 状态枚举：`ACTIVE`（正常）、`DISABLED`（禁用）、`DELETED`（已删除）。
> 使用 service token 鉴权，适合后台管理系统调用。

### 删除用户（软删除）

```typescript
await authClient.deleteUser('user-uuid');
```

> 软删除：将用户状态设为 `DELETED`，保留数据用于审计，不物理删除。等效于 `setUserStatus(userId, 'DELETED')`。

### 管理端修改用户信息

```typescript
// 修改用户名
await authClient.adminUpdateUser('user-uuid', { username: 'new_username' });

// 修改手机号
await authClient.adminUpdateUser('user-uuid', { mobile: '13900139001' });

// 修改真实姓名
await authClient.adminUpdateUser('user-uuid', { realname: '张三' });

// 同时修改多个字段
await authClient.adminUpdateUser('user-uuid', {
  username: 'new_username',
  mobile: '13900139001',
  realname: '张三',
});
```

> 使用 service token 鉴权，无需短信验证码，适合后台管理系统调用。
> `username` 和 `mobile` 唯一，冲突时抛出 409 错误（`err.authCode` 对应业务错误码）。

---

## 框架中间件

### Koa

```typescript
import Koa from 'koa';
import { AuthClient, koaAuthMiddleware } from '@scorehub/auth-sdk';

const app = new Koa();
const authClient = new AuthClient({ /* ... */ });

app.use(koaAuthMiddleware(authClient, {
  exclude: ['/health', '/api/v1/auth/login'],  // 不需认证的路径
}));

// 路由中获取用户信息
app.use(async (ctx) => {
  const user = ctx.state.user;    // TokenPayload
  const admin = ctx.state.admin;  // 兼容旧代码: admin._id / admin.username
});
```

### Express

```typescript
import express from 'express';
import { AuthClient, expressAuthMiddleware } from '@scorehub/auth-sdk';

const app = express();
const authClient = new AuthClient({ /* ... */ });

app.use(expressAuthMiddleware(authClient, {
  exclude: ['/health'],
}));

// 路由中获取用户信息
app.get('/profile', (req, res) => {
  const user = (req as any).user; // TokenPayload
  res.json(user);
});
```

### NestJS

```typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { AuthClient, AUTH_CLIENT } from '@scorehub/auth-sdk';

@Module({
  providers: [
    {
      provide: AUTH_CLIENT,
      useFactory: () => new AuthClient({
        serviceUrl: process.env.AUTH_SERVICE_URL,
        clientId: process.env.AUTH_CLIENT_ID,
        clientSecret: process.env.AUTH_CLIENT_SECRET,
        publicKey: fs.readFileSync('./keys/public.pem'),
      }),
    },
  ],
  exports: [AUTH_CLIENT],
})
export class AppModule {}

// controller
import { UseGuards } from '@nestjs/common';
import { AuthGuard } from '@scorehub/auth-sdk';

@UseGuards(AuthGuard)
@Controller('users')
export class UsersController {
  @Get('me')
  getProfile(@Request() req) {
    return req.user; // TokenPayload
  }
}
```

---

## Token 提取规则

中间件按以下优先级提取 Token：

| 优先级 | 来源 |
|--------|------|
| 1 | `Authorization: Bearer <token>` |
| 2 | `x-access-token` Header |
| 3 | `access-token` Header（仅 Koa）|
| 4 | `access_token` Header（仅 Koa）|
| 5 | `?token=` Query 参数 |

---

## 错误处理

所有方法在失败时抛出带 `authCode` 属性的 Error：

```typescript
try {
  await authClient.loginByPassword(username, password);
} catch (err: any) {
  console.log(err.message);   // 错误描述
  console.log(err.authCode);  // 业务错误码，如 1001 (Unauthorized)
}
```