ClouisleClouisle

Users API

管理当前用户资料与密码,以及管理员的用户 CRUD、邮件与密码生命周期

Users API 分成两组:当前用户自助(/api/v1/users/me/*,任何已认证用户)和管理员用户管理(/api/v1/admin/users/*,需要 admin:user:* 权限码)。

概述

  • 当前用户:GET|PUT /api/v1/users/me、POST /api/v1/users/me/change-password、GET /api/v1/users/me/password-status、DELETE /api/v1/users/me
  • 管理员:/api/v1/admin/users 下的列表、统计、详情、创建、更新、删除、激活/停用、批量发信,以及密码生命周期端点

认证

所有端点使用 Authorization: Bearer <JWT>。API Key 不能用于用户管理端点。

管理员端点额外需要以下权限码:

权限码用途
admin:user:read列表、详情、用户统计、密码过期统计与过期用户列表
admin:user:create创建用户
admin:user:update更新用户、激活/停用、批量发信、全部密码生命周期操作
admin:user:delete删除用户

超级管理员绕过权限码检查。权限不足返回 HTTP 403 / 3000。

当前用户端点

获取当前用户

GET /api/v1/users/me
curl -X GET "https://your-domain.com/api/v1/users/me" \
  -H "Authorization: Bearer YOUR_TOKEN"

成功响应(200 OK):

{
  "code": 0,
  "data": {
    "id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
    "username": "johndoe",
    "email": "john.doe@example.com",
    "is_active": true,
    "approval_status": "approved",
    "is_superuser": false,
    "email_verified": true,
    "avatar_url": "https://example.com/avatars/johndoe.png",
    "locale": "en",
    "created_at": "2026-01-15T10:00:00Z",
    "last_login": "2026-09-24T14:30:00Z",
    "auth_source": "local",
    "external_id": null,
    "force_password_change": false,
    "password_expiration_exempt": false,
    "status": "active",
    "roles": [
      {
        "id": "9d8e4f7a-8b6c-4d5e-a1f2-3b4c5d6e7f80",
        "name": "user",
        "description": null,
        "is_system_role": true,
        "permissions": []
      }
    ],
    "sso_connections": [
      {
        "id": "4d5e6f70-1a2b-4c3d-9e8f-7a6b5c4d3e2f",
        "provider_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "provider_name": "github",
        "provider_display_name": "GitHub",
        "provider_icon_url": null,
        "provider_user_id": "12345678",
        "provider_username": "johndoe",
        "provider_email": "john.doe@example.com",
        "first_login": "2026-02-01T08:00:00Z",
        "last_login": "2026-09-24T14:30:00Z"
      }
    ]
  },
  "msg": "success"
}

响应中的 status 是响应模型的默认值,这些端点始终返回 "active",不反映真实账号状态。判断账号是否可用请用 is_active(是否启用)与 approval_status(approved / pending)的组合;前端列表也是在这两个字段上派生 active/inactive/pending 三类。

更新当前用户

PUT /api/v1/users/me
字段类型必填说明
usernamestring否新用户名,必须全局唯一
emailstring否新邮箱,必须全局唯一;站点开启邮箱验证时需要同时提交验证码
email_verification_codestring否修改邮箱时的验证码;仅用于校验,不会写入用户资料
avatar_urlstring否头像 URL
localestring否界面语言,如 en、zh
curl -X PUT "https://your-domain.com/api/v1/users/me" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"username": "johnsmith", "locale": "zh"}'

失败路径:

HTTPcode场景
4005002用户名已被占用
4005003邮箱已被占用
4005005修改邮箱的验证码不正确(开启邮箱验证时)

成功响应返回更新后的完整用户对象(结构同「获取当前用户」),msg 为 Profile updated successfully。

修改邮箱不会自动刷新 email_verified:如果站点启用了邮箱验证,必须先完成验证码校验再提交新邮箱,否则下次登录可能被 5004(EMAIL_NOT_VERIFIED)拦下。

修改密码

POST /api/v1/users/me/change-password
字段类型必填说明
current_passwordstring是当前密码
new_passwordstring是新密码
curl -X POST "https://your-domain.com/api/v1/users/me/change-password" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"current_password": "OldPassword123!", "new_password": "NewPassword456!"}'

修改成功会同时清零 force_password_change 标记、写入新的密码修改时间并发送安全通知。

失败路径:

HTTPcode场景
4002003当前密码不正确
4005306未达到最小密码年龄(password_min_age_days),暂不能修改
4001001新密码不符合密码策略,data.errors.new_password 列出具体原因

密码策略由站点设置决定(默认值):长度至少 min_password_length = 8,必须含大写字母(require_uppercase = true)、必须含数字(require_number = true),特殊字符默认不要求(require_special_char = false)。修改密码时还会比对最近 password_history_count = 5 个历史密码,命中则报错 password_recently_used。

成功响应(200 OK):

{ "code": 0, "data": null, "msg": "Password changed successfully" }

查询密码状态

GET /api/v1/users/me/password-status

用于在「账号安全」页展示密码是否即将过期,避免登录时才被强制跳转。

{
  "code": 0,
  "data": {
    "is_exempt": false,
    "password_changed_at": "2026-06-01T08:00:00+00:00",
    "password_expires_at": "2026-08-30T08:00:00+00:00",
    "is_expired": false,
    "days_until_expiration": 12,
    "force_change_required": false
  },
  "msg": "success"
}
字段类型说明
is_exemptboolean是否豁免密码过期策略
password_changed_atstring | null上次修改时间(ISO 8601)
password_expires_atstring | null计算出的过期时间;未启用密码过期策略时为 null
is_expiredboolean是否已过期
days_until_expirationinteger | null距离过期天数;无过期时间时为 null
force_change_requiredboolean是否被管理员标记为必须修改密码

注销(删除)本人账号

DELETE /api/v1/users/me

请求体需要密码二次确认:

{ "password": "CurrentPassword123!" }
curl -X DELETE "https://your-domain.com/api/v1/users/me" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"password": "CurrentPassword123!"}'
HTTPcode场景
4002003密码不正确
4003000站点关闭了自助注销(allow_account_deletion = false),或账号是超级管理员

注销不可逆。成功响应为 {"code": 0, "data": null, "msg": "Account deleted successfully"},随后该账号的令牌与 API Key 都会失效。管理员如需保留账号,请改用「停用」而不是删除。

管理员端点

列出用户

GET /api/v1/admin/users
查询参数类型默认值说明
pageinteger1页码
page_sizeinteger20每页条数
statusstring[]—active(is_active = true)、inactive(停用且已批准)、pending(停用且待审批)(可重复)
searchstring—用户名或邮箱模糊匹配
rolestring[]—按角色名过滤(可重复,自动去重)
exclude_user_idUUID[]—排除指定用户(可重复)
curl -X GET "https://your-domain.com/api/v1/admin/users?page=1&page_size=20&status=pending" \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN"

分页响应结构为 { items, total, page, page_size },items 中每项与「获取当前用户」一致。

用户统计

GET /api/v1/admin/users/stats

需要 admin:user:read:

{
  "code": 0,
  "data": { "total": 156, "active": 150, "inactive": 2, "pending": 4 },
  "msg": "success"
}

total 是全部用户数,active 只统计 is_active = true;inactive 与 pending 都要求 is_active = false,按 approval_status 区分。

获取用户详情

GET /api/v1/admin/users/{user_id}

返回单个用户对象;不存在返回 HTTP 404 / 4001。

创建用户

POST /api/v1/admin/users
字段类型必填默认值说明
usernamestring是—用户名,必须唯一
emailstring是—邮箱,必须唯一
passwordstring是—初始密码,按密码策略校验
is_activeboolean否true是否直接启用
is_superuserboolean否false是否超级管理员
avatar_urlstring否null头像 URL
localestring否站点默认语言界面语言
curl -X POST "https://your-domain.com/api/v1/admin/users" \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"username": "alice", "email": "alice@example.com", "password": "SecurePassword123"}'

管理员创建的用户总是 approval_status = "approved":请求里携带的 approval_status 会被忽略。若希望新账号先待审批,请改用开放注册流程。terms_accepted、captcha_id、captcha_token 属于注册接口字段,不要在本接口提交。

失败路径:用户名重复 HTTP 400 / 5002;邮箱重复 HTTP 400 / 5003;密码不满足策略 HTTP 400 / 1001。

创建成功会返回完整用户对象(roles 为空数组),并记录审计日志。

更新用户

PUT /api/v1/admin/users/{user_id}

所有字段可选:

字段类型说明
emailstring新邮箱
passwordstring重置密码(按密码策略校验,但不检查历史密码)
is_activeboolean启用或停用
avatar_urlstring头像 URL
localestring界面语言
rolesstring[]角色名列表,整体替换现有角色

管理员不能通过本接口修改 username、is_superuser 或 approval_status——这些字段不在请求模型中,传了会被忽略。用户名只能在用户自助的 PUT /users/me 中修改。

角色替换有一条安全约束:如果被修改的用户当前是某个团队的 owner 或 admin,而新角色集合中没有任何角色带 team:manage 权限,请求会被拒绝,返回 HTTP 403 / 3000(permission 字段为 team:manage)。这是为了避免团队管理者失去管理权限后团队无人可管。

删除用户

DELETE /api/v1/admin/users/{user_id}
  • 用户不存在:HTTP 404 / 4001
  • 目标是超级管理员:HTTP 400 / 5201(CANNOT_DELETE_SUPERUSER)

响应中 data 是被删除的用户对象(用于审计回显),不是 null。

激活 / 停用用户

POST /api/v1/admin/users/{user_id}/activate
POST /api/v1/admin/users/{user_id}/deactivate

两者都返回更新后的用户对象。状态不变时报错:

HTTPcode场景
4005212对已激活用户调用 activate
4005213对已停用用户调用 deactivate
4005214试图停用超级管理员
4044001用户不存在

批量发送邮件

POST /api/v1/admin/users/send-email

需要 admin:user:update 与已启用的 SMTP。

字段类型必填说明
subjectstring是邮件主题
contentstring是邮件正文(纯文本,发送时会按换行渲染进 HTML 模板)
user_idsUUID[]是收件用户列表
curl -X POST "https://your-domain.com/api/v1/admin/users/send-email" \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"subject": "维护通知", "content": "系统将于周六维护。", "user_ids": ["3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c"]}'

额度与限流:每个管理员每小时最多 100 封;同一收件邮箱每天最多 5 封,超额收件人被跳过而不是整批失败。

{
  "code": 0,
  "data": { "sent_count": 1, "skipped_count": 0, "total": 1 },
  "msg": "Emails have been queued for sending"
}
HTTPcode场景
4005007SMTP 未配置
4005400发信额度不足;data 为 {"limit": 100, "period": "hour"} 或 {"requested": 3, "remaining": 1}
4004000user_ids 中没有任何有效用户

发送在后台任务中执行,响应只表示已入队,不代表已投递成功。

密码生命周期管理(管理员)

没有「管理员直接重置密码」的独立端点。重置密码请用 PUT /api/v1/admin/users/{user_id} 的 password 字段;以下端点用于管理密码过期策略,除标注外均需 admin:user:update。

强制修改密码

POST /api/v1/admin/users/{user_id}/force-password-change

置位 force_password_change,用户下次登录时会被要求修改密码(登录仍返回 code: 0,但带 force_password_change: true 与 reason)。响应 200 OK,data: null。

重置密码过期时间

POST /api/v1/admin/users/{user_id}/reset-password-expiration

以当前时间重新计算过期时间,相当于「确认用户刚改过密码」。响应 200 OK,data: null。

设置过期豁免

POST /api/v1/admin/users/{user_id}/exempt-password-expiration
{ "exempt": true }

用于服务账号或不适合强制轮换的账号。响应 200 OK,data: null。

批量强制修改密码

POST /api/v1/admin/users/bulk-force-password-change
{ "user_ids": ["3f2a1b0c-...", "9d8e4f7a-..."] }

响应 data.count 为实际更新的用户数。user_ids 为空返回 400 / 1001;全部 ID 都无效返回 400 / 4001。

密码过期统计

GET /api/v1/admin/users/password-expiration-stats

需要 admin:user:read。响应包含 total_users、expired_count、expiring_soon_count、force_change_count、exempt_count。

当站点未启用密码过期策略(password_expiration_enabled = false)时,该接口直接返回全 0,不会计算真实数字。前端应同时判断策略开关,避免把 0 解读成「无人即将过期」。统计只覆盖 auth_source = "local" 的账号(SSO 账号不参与过期策略)。

列出密码过期用户

GET /api/v1/admin/users/expiring-passwords?page=1&page_size=20&filter=expiring

需要 admin:user:read。

参数类型默认值说明
filterstringallall、expired、expiring、force_change 之一
pageinteger1页码,≥ 1
page_sizeinteger20每页条数,范围 1–100

路由顺序注意:/api/v1/admin/users/stats、/send-email、/bulk-force-password-change、/password-expiration-stats、/expiring-passwords 都是固定路径,必须写在 /{user_id} 之前,否则会被当成用户 ID。使用客户端 SDK 时无需关心,手写路径时不要漏。

错误码

HTTPcode场景
4001001请求校验失败(密码策略、空 user_ids 等)
4002003当前密码错误(修改密码、自助注销)
4032003JWT 无法解析或已过期
4003000自助注销被禁用,或试图删除自己的超级管理员账号
4033000缺少 admin:user:* 权限,或角色替换会使团队管理员失去 team:manage
4044001用户不存在
4005002用户名已被占用
4005003邮箱已被占用
4005005修改邮箱的验证码无效
4005007批量发信时 SMTP 未配置
4005201不能删除超级管理员
4005212用户已处于激活状态
4005213用户已处于停用状态
4005214不能停用超级管理员
4005306未达到最小密码年龄
4005400发信额度不足

最佳实践

资料与安全

  • 修改邮箱前先完成验证码校验,避免登录被 5004 拦截。
  • 用 GET /me/password-status 提前提示密码即将过期,而不是等登录时被迫跳转。
  • 自助注销前确认站点的 allow_account_deletion 设置;企业环境通常应关闭并用「停用」替代。

不要共享账号、不要用弱密码、不要跳过邮箱验证,也不要用超级管理员账号做日常操作。

管理员操作

  • 创建用户时使用强初始密码,并按需开启「首次登录强制修改密码」。
  • 停用优先于删除:停用可逆,删除会同时失去审计关联。
  • 调整角色时先确认目标用户是否管理着团队,避免触发 team:manage 约束。
  • 批量发信前核对 user_ids,并注意每收件人每天 5 封的上限。

代码示例

Python

import os
import requests

BASE = "https://your-domain.com/api/v1"


def get_password_status(token: str) -> dict:
    """查询当前用户的密码状态,用于提前提醒过期。"""
    response = requests.get(
        f"{BASE}/users/me/password-status",
        headers={"Authorization": f"Bearer {token}"},
    )
    result = response.json()
    if result["code"] != 0:
        raise RuntimeError(f"{result['code']} {result['msg']}")
    return result["data"]


def change_password(token: str, current_password: str, new_password: str) -> None:
    """修改当前用户密码。"""
    response = requests.post(
        f"{BASE}/users/me/change-password",
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
        json={"current_password": current_password, "new_password": new_password},
    )
    result = response.json()
    if result["code"] != 0:
        # 1001 时 data.errors.new_password 会列出具体策略原因
        raise RuntimeError(f"{result['code']} {result['msg']} {result.get('data')}")


def bulk_send_email(admin_token: str, subject: str, content: str, user_ids: list[str]) -> dict:
    """管理员批量发信,返回入队统计。"""
    response = requests.post(
        f"{BASE}/admin/users/send-email",
        headers={"Authorization": f"Bearer {admin_token}", "Content-Type": "application/json"},
        json={"subject": subject, "content": content, "user_ids": user_ids},
    )
    result = response.json()
    if result["code"] != 0:
        raise RuntimeError(f"{result['code']} {result['msg']}")
    return result["data"]


# 使用
token = os.environ["USER_TOKEN"]
status = get_password_status(token)
print(f"密码 {status['days_until_expiration']} 天后过期")

JavaScript

const BASE = 'https://your-domain.com/api/v1';

async function getCurrentUser(token) {
  const response = await fetch(`${BASE}/users/me`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  const result = await response.json();
  if (result.code !== 0) throw new Error(`${result.code} ${result.msg}`);
  return result.data;
}

async function updateProfile(token, { username, locale, avatarUrl } = {}) {
  const response = await fetch(`${BASE}/users/me`, {
    method: 'PUT',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ username, locale, avatar_url: avatarUrl }),
  });
  const result = await response.json();
  if (result.code !== 0) throw new Error(`${result.code} ${result.msg}`);
  return result.data;
}

async function setUserActive(adminToken, userId, active) {
  const action = active ? 'activate' : 'deactivate';
  const response = await fetch(`${BASE}/admin/users/${userId}/${action}`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${adminToken}` },
  });
  const result = await response.json();
  if (result.code !== 0) throw new Error(`${result.code} ${result.msg}`);
  return result.data;
}

// 使用
const token = process.env.USER_TOKEN;
const user = await getCurrentUser(token);
console.log('user:', user.username);

相关文档

这篇文章对你有帮助吗?

本页目录