Fizzy Identity API 实战指南:身份账户查询与时区更新
【免费下载链接】fizzyKanban as it should be. Not as it has been.项目地址: https://gitcode.com/GitHub_Trending/fizzy2/fizzy
导读
Fizzy 是一个多租户看板(Kanban)应用,而Identity(身份)是它账号体系的基石——它代表的不是"某个工作区里的成员",而是"使用 Fizzy 的某个人",可以横跨多个账户(Account)存在。本文围绕官方 API 文档中 Identity 的两个核心端点展开:用GET /my/identity一次获取当前身份可访问的全部账户及其用户信息,用PATCH /:account_slug/my/timezone调整当前用户的时区偏好。读完本文,你将能够:在自己的应用或脚本中完成"启动时枚举账户"这一典型集成动作,并理解时区设置从 API 请求到通知邮件渲染的完整链路。文中所有接口细节均以官方文档为主干,并结合 Fizzy 仓库源码(模型、控制器、jbuilder 视图与路由)进行深度印证。
一、什么是 Identity:跨越账户的人
在深入接口之前,先建立一个准确的概念模型。官方文档对 Identity 的定义非常简洁:
An Identity represents a person using Fizzy.
在 Fizzy 的多租户设计中,人是全局的,账户是局部的。一个 Identity 可以加入多个账户,在每个账户中对应一条独立的 User 记录。源码 app/models/identity.rb 印证了这一关系:
class Identity < ApplicationRecord include Joinable, Transferable has_many :users, dependent: :nullify has_many :accounts, through: :users end其中Joinable(app/models/identity/joinable.rb)提供了把身份加入账户的核心方法:
def join(account, **attributes) attributes[:name] ||= email_address transaction do account.users.find_or_create_by!(identity: self) do |user| user.assign_attributes(attributes) end.previously_new_record? end end一个值得注意的细节:每个账户下的 User 记录默认以email_address作为名字——这正好解释了本文后面时区端点中 "current user" 的语义:时区是按账户内用户(User)存储的,而身份(Identity)本身不持有时区。
多租户上下文通过 app/models/current.rb 贯穿整个请求周期:Current.session→Current.identity→Current.user(按当前账户从 identity 的 users 中解析),构成了"一次登录、多账户切换"的基础。
二、GET /my/identity:获取身份与账户清单
2.1 端点概览
GET /my/identity该端点不需要账户上下文(即不需要先访问某个/account_slug/...路径),返回当前登录身份可访问的所有账户,以及每个账户对应的用户信息。路由定义位于 config/routes.rb 的namespace :my块中:
namespace :my do resource :identity, only: :show resource :timezone # ... end控制器 app/controllers/my/identities_controller.rb 极其精简,但有一处关键声明:
class My::IdentitiesController < ApplicationController disallow_account_scope def show @identity = Current.identity end enddisallow_account_scope(定义于 app/controllers/concerns/authentication.rb)会跳过require_account前置钩子并拦截带账户上下文的请求,从而保证该端点只依赖登录身份本身,与"当前处于哪个账户"无关。
2.2 身份认证方式
该端点既支持个人访问令牌(Personal Access Token),也支持会话 Cookie:
# 方式一:Bearer 令牌 curl -H "Authorization: Bearer put-your-access-token-here" \ -H "Accept: application/json" \ https://app.fizzy.do/my/identity # 方式二:会话 Cookie(magic link 登录后) curl -H "Cookie: session_token=eyJfcmFpbHMi..." \ -H "Accept: application/json" \ https://app.fizzy.do/my/identityBearer 令牌的校验逻辑在 app/controllers/concerns/authentication.rb 的authenticate_by_bearer_token中:仅对 JSON 请求生效,并通过Identity.find_by_permissable_access_token(token, method: request.method)(app/models/identity.rb)按 HTTP 方法校验令牌的读写权限。更完整的令牌申请、列举与删除流程见 docs/api/sections/authentication.md。
2.3 响应结构逐字段解读
官方文档给出的响应示例:
{ "accounts": [ { "id": "03f5v9zjskhcii2r45ih3u1rq", "name": "37signals", "slug": "/897362094", "created_at": "2025-12-05T19:36:35.377Z", "user": { "id": "03f5v9zjw7pz8717a4no1h8a7", "name": "David Heinemeier Hansson", "role": "owner", "active": true, "email_address": "david@example.com", "created_at": "2025-12-05T19:36:35.401Z", "url": "http://app.fizzy.localhost:3006/users/03f5v9zjw7pz8717a4no1h8a7" } } ] }这个 JSON 并非手写示例,而是由 jbuilder 模板逐字段渲染出来的,源码清晰可查。
外层响应由 app/views/my/identities/show.json.jbuilder 生成:
json.id @identity.id json.accounts @identity.users_with_active_accounts do |user| json.partial! "my/identities/account", account: user.account json.user user, partial: "users/user", as: :user end两个值得注意的实现细节:
- 只返回"活跃账户":
users_with_active_accounts(app/models/identity.rb)通过users.joins(:account).merge(Account.active)过滤,已关闭(deactivated)的账户不会出现在列表中。 - 响应带顶层
id:文档示例省略了顶层id(即 Identity 自身的 ULID),但实际渲染会输出json.id @identity.id。
账户片段由 app/views/my/identities/_account.json.jbuilder 渲染:
json.cache! account do json.(account, :id, :name, :slug) json.created_at account.created_at.utc end用户片段由 app/views/users/_user.json.jbuilder 渲染:
json.cache! user do json.(user, :id, :name, :role, :active) json.email_address user.identity&.email_address json.created_at user.created_at.utc json.url user_url(user) json.avatar_url user_avatar_url(user) end各字段含义与数据来源汇总如下:
| 字段 | 类型 | 含义 | 来源 |
|---|---|---|---|
id | string | 账户/用户/身份的 ULID 主键 | 模型主键 |
name | string | 账户名 / 用户名 | Account / User |
slug | string | 账户标识(形如/897362094,用于构造账户级 API 路径) | Account |
created_at | datetime | 创建时间(UTC) | 模型时间戳 |
role | string | 用户在账户内的角色:owner/admin/member/system | app/models/user/role.rb 枚举 |
active | boolean | 用户是否处于激活状态 | User#active |
email_address | string | 身份邮箱(跨账户共享) | user.identity&.email_address |
url | string | 用户页面对外 URL | user_url(user) |
avatar_url | string | 用户头像 URL(响应中实际存在,文档示例未列出) | user_avatar_url(user) |
角色枚举定义在 app/models/user/role.rb:
enum :role, %i[ owner admin member system ].index_by(&:itself), scopes: false同时提供admin?(super || owner?)等权限判断,owner是账户内的最高角色,本文开头示例中 DHH 在 "37signals" 账户中的角色正是owner。
2.4 典型使用场景
该端点是客户端集成的"入口端点",最常见的用途是应用/脚本启动时枚举用户可用的全部账户,随后再针对具体账户发起业务请求(如/1234567/boards、/1234567/cards)。配合slug字段(如/897362094)即可拼出账户作用域路径。由于响应中account片段启用了json.cache!,并且控制器层通过etag { Current.identity.id }提供 ETag(app/controllers/concerns/authentication.rb),客户端可配合If-None-Match做增量缓存,账户列表未变化时直接命中304 Not Modified,大幅减少重复拉取开销(ETag 用法详见 docs/api/README.md)。
三、PATCH /:account_slug/my/timezone:更新当前用户时区
3.1 端点概览
PATCH /:account_slug/my/timezone该端点更新当前账户上下文内当前用户的时区。官方文档明确说明其作用范围:影响通知邮件(notification emails)中时间的显示方式。
参数定义如下(完全继承自官方文档):
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
timezone_name | string | 是 | IANA 时区标识符(如America/New_York、Europe/London、Asia/Tokyo) |
请求示例:
{ "timezone_name": "America/New_York" }成功时返回204 No Content。
3.2 源码实现链路
控制器 app/controllers/my/timezones_controller.rb 是整条链路的入口:
class My::TimezonesController < ApplicationController def update Current.user.settings.update!(timezone_name: timezone_param) head :no_content end private def timezone_param params[:timezone_name] end end可以看到,时区并非存储在 Identity 上,而是落在账户内用户(User)的 Settings 记录上。User通过Configurable模块(app/models/user/configurable.rb)维护这份设置:
has_one :settings, class_name: "User::Settings", dependent: :destroy after_create :create_settings, unless: :system? delegate :timezone, to: :settings, allow_nil: true def time_zone(&block) Time.use_zone(timezone, &block) end也就是说,每个用户创建时会自动生成一份User::Settings,而timezone是这份设置的动态计算结果。
3.3 时区解析与默认值
设置的实际存储与解析逻辑位于 app/models/user/settings.rb:
def timezone if timezone_name.present? ActiveSupport::TimeZone[timezone_name] || default_timezone else default_timezone end end def default_timezone ActiveSupport::TimeZone["UTC"] end两个关键行为:
- IANA 标识符解析:
timezone_name存的是America/New_York这类 IANA 名称,通过ActiveSupport::TimeZone[...]解析为 Ruby 的时区对象(TimeZone内部即映射 IANA 名称)。 - 容错回退:若传入的名称无法解析(拼写错误或不存在),不会抛异常,而是回退到默认时区UTC。未设置时同样默认 UTC。
3.4 时区如何影响通知邮件
timezone_name的语义在上游被Time.use_zone消费:凡是需要在当前用户时区下渲染时间的代码,都会通过user.time_zone { ... }包裹执行(见 app/models/user/configurable.rb)。官方文档所述的"影响通知邮件中的时间显示"正是通过这条委托链实现的——邮件模板在渲染时间戳时进入用户时区,从而显示为当地时间而非服务器 UTC 时间。
需要注意的是,该设置按账户内用户生效:同一个 Identity 在不同账户下有各自的 User 与 Settings,因此时区偏好天然隔离,不会跨账户串扰。
3.5 路由与作用域
路由同样定义在namespace :my中(config/routes.rb):
resource :timezoneresource单数形式意味着这是一条单例资源路由——用户没有"多个时区",只有一个偏好设置。完整路径为PATCH /:account_slug/my/timezone,其中:account_slug是账户级作用域前缀(如/897362094),与GET /my/identity这类全局端点在作用域上形成互补:一个是"账户无关的全局视图",一个是"账户内的用户偏好"。
四、实践建议与注意事项
- 启动流程推荐组合:先
GET /my/identity拿到账户清单(含 slug),再逐账户调用业务 API;Identity 顶层id可用于客户端本地关联身份缓存。 - 时区名称必须使用 IANA 标识符:不要传
EST、GMT+8这类缩写或偏移量,ActiveSupport::TimeZone按 IANA 名称解析,非法值会静默回退为 UTC——排查"时区没生效"时优先检查拼写。 - 区分全局端点与账户端点:
GET /my/identity无账户前缀且禁用了账户作用域(disallow_account_scope);而时区端点必须携带:account_slug,二者不可混用。 - 响应缓存:
GET /my/identity的账户片段启用了片段缓存且响应带 ETag,高频轮询应使用If-None-Match以节省带宽。 - 权限提示:该端点的语义对象是"当前用户",即认证身份在当前账户下的 User 记录;如果请求未携带有效账户上下文,会因
require_account失败而无法命中,因此调用前请确认已先进入目标账户作用域(如通过/1234567前缀的请求建立账户上下文)。
五、小结
Identity 是 Fizzy 多租户账号体系的核心抽象:一个人(Identity)跨多个账户(Account),每个账户内体现为一条 User 记录。GET /my/identity是这个体系面向 API 的窗口——一次请求即可枚举身份的全部活跃账户及其用户信息,适合作为集成客户端的启动入口;PATCH /:account_slug/my/timezone则把 IANA 时区偏好写入账户内用户的 Settings,经ActiveSupport::TimeZone解析、Time.use_zone消费,最终体现在通知邮件的本地化时间渲染上。两个端点一"全局"一"账户内",共同构成了 Fizzy 身份层对外部应用最基本的两个操作。想要进一步探索身份机制,推荐继续阅读 app/models/identity.rb、app/models/current.rb 与 docs/api/sections/authentication.md。
【免费下载链接】fizzyKanban as it should be. Not as it has been.项目地址: https://gitcode.com/GitHub_Trending/fizzy2/fizzy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考