pinme-auth
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePinMe Worker Auth API Integration
PinMe Worker Auth API 集成指南
Guides how to call PinMe platform's Identity Platform auth proxy APIs in a PinMe Worker (TypeScript).
本文指导如何在PinMe Worker(TypeScript)中调用PinMe平台的Identity Platform认证代理API。
Environment Variables
环境变量
typescript
// backend/src/worker.ts
export interface Env {
DB: D1Database;
API_KEY: string; // 项目 API Key — 用于所有 auth 接口认证
PROJECT_NAME: string; // 项目名 — 所有 auth 接口必须同时传递
BASE_URL?: string; // 可选,默认 https://pinme.cloud
}和API_KEY是所有 auth 接口的必填凭证,缺一不可。PROJECT_NAME
typescript
// backend/src/worker.ts
export interface Env {
DB: D1Database;
API_KEY: string; // 项目 API Key — 用于所有 auth 接口认证
PROJECT_NAME: string; // 项目名 — 所有 auth 接口必须同时传递
BASE_URL?: string; // 可选,默认 https://pinme.cloud
}和API_KEY是所有 auth 接口的必填凭证,缺一不可。PROJECT_NAME
认证方式(所有接口通用)
认证方式(所有接口通用)
| 参数 | 传递方式 | 必填 | 说明 |
|---|---|---|---|
| 请求头 | 是 | 项目 API Key |
| Query 参数 | 是 | 必须与 |
服务端会先校验这两个字段是否匹配同一个项目,再从项目配置中取出 ,然后转调 Identity Platform。
tenant_id| 参数 | 传递方式 | 必填 | 说明 |
|---|---|---|---|
| 请求头 | 是 | 项目 API Key |
| Query 参数 | 是 | 必须与 |
服务端会先校验这两个字段是否匹配同一个项目,再从项目配置中取出 ,然后转调 Identity Platform。
tenant_id通用错误
通用错误
| 场景 | HTTP | |
|---|---|---|
缺少 | 401 | |
缺少 | 400 | |
| API Key 和项目不匹配 | 401 | |
| 项目未配置认证租户 | 400 | |
| 场景 | HTTP | |
|---|---|---|
缺少 | 401 | |
缺少 | 400 | |
| API Key 和项目不匹配 | 401 | |
| 项目未配置认证租户 | 400 | |
通用 TypeScript 类型
通用 TypeScript 类型
typescript
type ApiEnvelope<T> = {
code: number // 200=成功,其他=失败
msg: string // "ok" | "fail" | "invalid param"
data: T
}
type ApiErrorData = { error?: string }
type UserInfo = {
uid: string
email: string
display_name: string
photo_url?: string
disabled: boolean
email_verified: boolean
}typescript
type ApiEnvelope<T> = {
code: number // 200=成功,其他=失败
msg: string // "ok" | "fail" | "invalid param"
data: T
}
type ApiErrorData = { error?: string }
type UserInfo = {
uid: string
email: string
display_name: string
photo_url?: string
disabled: boolean
email_verified: boolean
}API 1: 创建用户
API 1: 创建用户
Endpoint:
POST {BASE_URL}/api/v1/auth/create_user?project_name={project_name}仅用于邮箱密码注册。成功时用户已创建且验证邮件已发出;失败时自动回滚,不会留下僵尸账号。
创建成功后用户默认仍是"未验证"状态,需点击邮件验证链接后,才能通过校验。verify_token
接口地址:
POST {BASE_URL}/api/v1/auth/create_user?project_name={project_name}仅用于邮箱密码注册。成功时用户已创建且验证邮件已发出;失败时自动回滚,不会留下僵尸账号。
创建成功后用户默认仍是"未验证"状态,需点击邮件验证链接后,才能通过校验。verify_token
请求体
请求体
json
{ "email": "alice@example.com", "password": "Test@12345678", "display_name": "Alice" }| 字段 | 类型 | 必填 |
|---|---|---|
| string | 是 |
| string | 是 |
| string | 否 |
json
{ "email": "alice@example.com", "password": "Test@12345678", "display_name": "Alice" }| 字段 | 类型 | 必填 |
|---|---|---|
| string | 是 |
| string | 是 |
| string | 否 |
错误
错误
| 场景 | HTTP | |
|---|---|---|
| 缺少 email/password | 400 | |
| 上游创建失败 | 502 | |
| 发送验证邮件失败 | 500 | |
| 场景 | HTTP | |
|---|---|---|
| 缺少 email/password | 400 | |
| 上游创建失败 | 502 | |
| 发送验证邮件失败 | 500 | |
TypeScript 示例
TypeScript 示例
typescript
async function createAuthUser(
env: Env,
payload: { email: string; password: string; display_name?: string }
): Promise<{ user?: UserInfo; error?: string }> {
const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';
const resp = await fetch(
`${baseUrl}/api/v1/auth/create_user?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,
{
method: 'POST',
headers: { 'X-API-Key': env.API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}
);
const result = await resp.json() as ApiEnvelope<UserInfo | ApiErrorData>;
if (!resp.ok || result.code !== 200) {
return { error: (result.data as ApiErrorData)?.error ?? result.msg };
}
return { user: result.data as UserInfo };
}typescript
async function createAuthUser(
env: Env,
payload: { email: string; password: string; display_name?: string }
): Promise<{ user?: UserInfo; error?: string }> {
const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';
const resp = await fetch(
`${baseUrl}/api/v1/auth/create_user?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,
{
method: 'POST',
headers: { 'X-API-Key': env.API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}
);
const result = await resp.json() as ApiEnvelope<UserInfo | ApiErrorData>;
if (!resp.ok || result.code !== 200) {
return { error: (result.data as ApiErrorData)?.error ?? result.msg };
}
return { user: result.data as UserInfo };
}API 2: 校验 id_token
API 2: 校验 id_token
Endpoint:
POST {BASE_URL}/api/v1/auth/verify_token?project_name={project_name}校验前端登录后拿到的 (邮箱密码或 Google 登录均适用)。
id_token注意: token 合法但邮箱未验证时返回 ,不是 。
403401接口地址:
POST {BASE_URL}/api/v1/auth/verify_token?project_name={project_name}校验前端登录后拿到的 (邮箱密码或 Google 登录均适用)。
id_token注意: token 合法但邮箱未验证时返回 ,不是 。
403401请求体
请求体
json
{ "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6..." }json
{ "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6..." }成功响应 data
成功响应 data
typescript
type VerifyTokenData = {
uid: string
email?: string
tenant_id: string
claims: Record<string, unknown>
}typescript
type VerifyTokenData = {
uid: string
email?: string
tenant_id: string
claims: Record<string, unknown>
}错误
错误
| 场景 | HTTP | |
|---|---|---|
缺少 | 400 | |
| token 无效或过期 | 401 | |
| 邮箱未验证 | 403 | |
| 场景 | HTTP | |
|---|---|---|
缺少 | 400 | |
| token 无效或过期 | 401 | |
| 邮箱未验证 | 403 | |
TypeScript 示例
TypeScript 示例
typescript
async function verifyAuthToken(
env: Env,
idToken: string
): Promise<{ uid?: string; email?: string; error?: string; emailNotVerified?: boolean }> {
const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';
const resp = await fetch(
`${baseUrl}/api/v1/auth/verify_token?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,
{
method: 'POST',
headers: { 'X-API-Key': env.API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ id_token: idToken }),
}
);
const result = await resp.json() as ApiEnvelope<VerifyTokenData | ApiErrorData>;
if (!resp.ok || result.code !== 200) {
const error = (result.data as ApiErrorData)?.error ?? result.msg;
return { error, emailNotVerified: resp.status === 403 };
}
const data = result.data as VerifyTokenData;
return { uid: data.uid, email: data.email };
}typescript
async function verifyAuthToken(
env: Env,
idToken: string
): Promise<{ uid?: string; email?: string; error?: string; emailNotVerified?: boolean }> {
const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';
const resp = await fetch(
`${baseUrl}/api/v1/auth/verify_token?project_name=${encodeURIComponent(env.PROJECT_NAME)}`,
{
method: 'POST',
headers: { 'X-API-Key': env.API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ id_token: idToken }),
}
);
const result = await resp.json() as ApiEnvelope<VerifyTokenData | ApiErrorData>;
if (!resp.ok || result.code !== 200) {
const error = (result.data as ApiErrorData)?.error ?? result.msg;
return { error, emailNotVerified: resp.status === 403 };
}
const data = result.data as VerifyTokenData;
return { uid: data.uid, email: data.email };
}API 3: 查询单个用户
API 3: 查询单个用户
Endpoint:
GET {BASE_URL}/api/v1/auth/user?project_name={project_name}&uid={uid}接口地址:
GET {BASE_URL}/api/v1/auth/user?project_name={project_name}&uid={uid}错误
错误
| 场景 | HTTP | |
|---|---|---|
缺少 | 400 | |
| 用户不存在 | 404 | |
| 上游查询失败 | 502 | |
| 场景 | HTTP | |
|---|---|---|
缺少 | 400 | |
| 用户不存在 | 404 | |
| 上游查询失败 | 502 | |
TypeScript 示例
TypeScript 示例
typescript
async function getAuthUser(env: Env, uid: string): Promise<{ user?: UserInfo; error?: string }> {
const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';
const resp = await fetch(
`${baseUrl}/api/v1/auth/user?project_name=${encodeURIComponent(env.PROJECT_NAME)}&uid=${encodeURIComponent(uid)}`,
{ method: 'GET', headers: { 'X-API-Key': env.API_KEY } }
);
const result = await resp.json() as ApiEnvelope<UserInfo | ApiErrorData>;
if (!resp.ok || result.code !== 200) {
return { error: (result.data as ApiErrorData)?.error ?? result.msg };
}
return { user: result.data as UserInfo };
}typescript
async function getAuthUser(env: Env, uid: string): Promise<{ user?: UserInfo; error?: string }> {
const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';
const resp = await fetch(
`${baseUrl}/api/v1/auth/user?project_name=${encodeURIComponent(env.PROJECT_NAME)}&uid=${encodeURIComponent(uid)}`,
{ method: 'GET', headers: { 'X-API-Key': env.API_KEY } }
);
const result = await resp.json() as ApiEnvelope<UserInfo | ApiErrorData>;
if (!resp.ok || result.code !== 200) {
return { error: (result.data as ApiErrorData)?.error ?? result.msg };
}
return { user: result.data as UserInfo };
}API 4: 列出用户(分页)
API 4: 列出用户(分页)
Endpoint:
GET {BASE_URL}/api/v1/auth/list_users?project_name={project_name}默认 ,最大 。通过 循环翻页。
max_results=1001000next_page_token接口地址:
GET {BASE_URL}/api/v1/auth/list_users?project_name={project_name}默认 ,最大 。通过 循环翻页。
max_results=1001000next_page_tokenQuery 参数
Query 参数
| 参数 | 必填 | 说明 |
|---|---|---|
| 是 | 项目名 |
| 否 | 分页游标 |
| 否 | 每页数量,1–1000 |
| 参数 | 必填 | 说明 |
|---|---|---|
| 是 | 项目名 |
| 否 | 分页游标 |
| 否 | 每页数量,1–1000 |
TypeScript 示例
TypeScript 示例
typescript
async function listAuthUsers(
env: Env,
options: { pageToken?: string; maxResults?: number } = {}
): Promise<{ users?: UserInfo[]; nextPageToken?: string; error?: string }> {
const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';
const url = new URL('/api/v1/auth/list_users', baseUrl);
url.searchParams.set('project_name', env.PROJECT_NAME);
if (options.pageToken) url.searchParams.set('page_token', options.pageToken);
if (options.maxResults) url.searchParams.set('max_results', String(options.maxResults));
const resp = await fetch(url.toString(), { method: 'GET', headers: { 'X-API-Key': env.API_KEY } });
const result = await resp.json() as ApiEnvelope<{ users: UserInfo[]; next_page_token?: string } | ApiErrorData>;
if (!resp.ok || result.code !== 200) {
return { error: (result.data as ApiErrorData)?.error ?? result.msg };
}
const data = result.data as { users: UserInfo[]; next_page_token?: string };
return { users: data.users, nextPageToken: data.next_page_token };
}
// 批量遍历所有用户示例
async function* iterAllUsers(env: Env) {
let pageToken: string | undefined;
do {
const { users, nextPageToken, error } = await listAuthUsers(env, { pageToken, maxResults: 1000 });
if (error) throw new Error(error);
for (const user of users ?? []) yield user;
pageToken = nextPageToken;
} while (pageToken);
}typescript
async function listAuthUsers(
env: Env,
options: { pageToken?: string; maxResults?: number } = {}
): Promise<{ users?: UserInfo[]; nextPageToken?: string; error?: string }> {
const baseUrl = env.BASE_URL ?? 'https://pinme.cloud';
const url = new URL('/api/v1/auth/list_users', baseUrl);
url.searchParams.set('project_name', env.PROJECT_NAME);
if (options.pageToken) url.searchParams.set('page_token', options.pageToken);
if (options.maxResults) url.searchParams.set('max_results', String(options.maxResults));
const resp = await fetch(url.toString(), { method: 'GET', headers: { 'X-API-Key': env.API_KEY } });
const result = await resp.json() as ApiEnvelope<{ users: UserInfo[]; next_page_token?: string } | ApiErrorData>;
if (!resp.ok || result.code !== 200) {
return { error: (result.data as ApiErrorData)?.error ?? result.msg };
}
const data = result.data as { users: UserInfo[]; next_page_token?: string };
return { users: data.users, nextPageToken: data.next_page_token };
}
// 批量遍历所有用户示例
async function* iterAllUsers(env: Env) {
let pageToken: string | undefined;
do {
const { users, nextPageToken, error } = await listAuthUsers(env, { pageToken, maxResults: 1000 });
if (error) throw new Error(error);
for (const user of users ?? []) yield user;
pageToken = nextPageToken;
} while (pageToken);
}前端集成(Firebase Auth)
前端集成(Firebase Auth)
create_workerpublic_client_configcreate_workerpublic_client_config两种 api_key 区分
两种 api_key 区分
| 字段 | 用途 | 是否可暴露到浏览器 |
|---|---|---|
| 项目 API Key,调用本文所有代理接口 | 不能,只给 Worker/服务端 |
| Firebase Web API Key,初始化前端登录 SDK | 可以 |
| 字段 | 用途 | 是否可暴露到浏览器 |
|---|---|---|
| 项目 API Key,调用本文所有代理接口 | 不能,只给 Worker/服务端 |
| Firebase Web API Key,初始化前端登录 SDK | 可以 |
public_client_config 字段说明
public_client_config 字段说明
| 字段 | 前端用途 |
|---|---|
| |
| |
| |
| |
| 字段 | 前端用途 |
|---|---|
| |
| |
| |
| |
前端 TypeScript 示例
前端 TypeScript 示例
typescript
import { initializeApp } from 'firebase/app'
import {
type Auth,
getAuth,
GoogleAuthProvider,
signInWithEmailAndPassword,
signInWithPopup,
} from 'firebase/auth'
type PublicClientConfig = {
tenant_id: string
auth_api_key: string
auth_domain: string
auth_project_id: string
}
export function createProjectAuth(config: PublicClientConfig): Auth {
const app = initializeApp({
apiKey: config.auth_api_key,
authDomain: config.auth_domain,
projectId: config.auth_project_id,
})
const auth = getAuth(app)
auth.tenantId = config.tenant_id // 必须设置,确保 token 归属正确租户
return auth
}
// 邮箱密码登录,返回 id_token
export async function loginWithEmail(auth: Auth, email: string, password: string): Promise<string> {
const credential = await signInWithEmailAndPassword(auth, email, password)
return credential.user.getIdToken()
}
// Google 登录,返回 id_token
export async function loginWithGoogle(auth: Auth): Promise<string> {
const credential = await signInWithPopup(auth, new GoogleAuthProvider())
return credential.user.getIdToken()
}
// 用法示例
// pinme create 会自动将 public_client_config 写入 frontend/src/utils/config.ts
import { public_client_config } from '../utils/config'
const auth = createProjectAuth(public_client_config)
const idToken = await loginWithGoogle(auth)
// 然后把 idToken 发给自己的 Worker,由 Worker 调用 verify_token前端只负责登录和拿,不要直接持有项目id_token。api_key必须由 Worker/服务端代调。verify_token由frontend/src/utils/config.ts自动生成,无需手动创建。pinme create
typescript
import { initializeApp } from 'firebase/app'
import {
type Auth,
getAuth,
GoogleAuthProvider,
signInWithEmailAndPassword,
signInWithPopup,
} from 'firebase/auth'
type PublicClientConfig = {
tenant_id: string
auth_api_key: string
auth_domain: string
auth_project_id: string
}
export function createProjectAuth(config: PublicClientConfig): Auth {
const app = initializeApp({
apiKey: config.auth_api_key,
authDomain: config.auth_domain,
projectId: config.auth_project_id,
})
const auth = getAuth(app)
auth.tenantId = config.tenant_id // 必须设置,确保 token 归属正确租户
return auth
}
// 邮箱密码登录,返回 id_token
export async function loginWithEmail(auth: Auth, email: string, password: string): Promise<string> {
const credential = await signInWithEmailAndPassword(auth, email, password)
return credential.user.getIdToken()
}
// Google 登录,返回 id_token
export async function loginWithGoogle(auth: Auth): Promise<string> {
const credential = await signInWithPopup(auth, new GoogleAuthProvider())
return credential.user.getIdToken()
}
// 用法示例
// pinme create 会自动将 public_client_config 写入 frontend/src/utils/config.ts
import { public_client_config } from '../utils/config'
const auth = createProjectAuth(public_client_config)
const idToken = await loginWithGoogle(auth)
// 然后把 idToken 发给自己的 Worker,由 Worker 调用 verify_token前端只负责登录和拿,不要直接持有项目id_token。api_key必须由 Worker/服务端代调。verify_token由frontend/src/utils/config.ts自动生成,无需手动创建。pinme create
典型调用链路
典型调用链路
邮箱密码注册流程:
- → 创建用户并发出验证邮件
create_user - 用户点击邮件链接完成验证
- 前端登录拿到
id_token - → 校验 token,取得
verify_tokenuid - 需要时再调 读取完整用户信息
getAuthUser
Google 登录流程:
- 前端完成 Google Sign-In,拿到
id_token - → 校验 token(无需调用
verify_token)create_user
邮箱密码注册流程:
- → 创建用户并发出验证邮件
create_user - 用户点击邮件链接完成验证
- 前端登录拿到
id_token - → 校验 token,取得
verify_tokenuid - 需要时再调 读取完整用户信息
getAuthUser
Google 登录流程:
- 前端完成 Google Sign-In,拿到
id_token - → 校验 token(无需调用
verify_token)create_user
易错点
易错点
| 错误 | 正确做法 |
|---|---|
只传 | 每个请求都要同时带 |
| 403 = 邮箱未验证,提示用户检查邮箱;401 才是 token 失效 |
| 创建成功只代表验证邮件已发,用户必须点击后才算验证 |
| 有 |
成功判断只看 | 同时判断 |
| 错误 | 正确做法 |
|---|---|
只传 | 每个请求都要同时带 |
| 403 = 邮箱未验证,提示用户检查邮箱;401 才是 token 失效 |
| 创建成功只代表验证邮件已发,用户必须点击后才算验证 |
| 有 |
成功判断只看 | 同时判断 |