Files
im_webapp/web/src/lib/api/qrcodes.ts
T

95 lines
4.5 KiB
TypeScript

// QR code REST endpoints (docs/IM-前后端对接文档.md §5.15.1). Confirmed LIVE against the real
// deployment (2026-09-08, two fresh test accounts) -- `resolveQRCode`'s real response shape nests
// `relationship`/`can_add`/`action`/`public_account` all under `user`, unlike the doc's own worked
// example which shows them as siblings of `user` at the top level. Also confirmed live: resolving a
// code against the SAME account that minted it is rejected (`400 MALFORMED_REQUEST`, `field:"token"`,
// message "malformed request" -- distinct from a genuinely garbage token, which instead gets message
// "qr code invalid" with no `details.field` at all) -- cross-account resolution works correctly.
//
// The security rationale for why this exists at all instead of just encoding a raw user id (doc line
// 1299): "二维码只含不透明、签名、带租户和用途约束的短期 Token,不直接放手机号、用户 ID 或登录
// Token" -- the QR image payload is `qr_payload` (an `im://qr?token=...&purpose=...` URI), not
// `qr_token` alone, matching the old app's own `im://qr` scheme (im_webapp/src/views/tinode-web.jsx
// confirmed this exact prefix check before ever sending a scanned string to `resolve`).
import { imPost } from './client';
interface QRCodeDisplayProfile {
user_id: string;
display_name: string;
public_account?: string;
}
export interface MyQRCode {
qr_token: string;
/** What actually gets encoded into the QR image -- `im://qr?token=...&purpose=...`. */
qr_payload: string;
expires_at: string;
display_profile: QRCodeDisplayProfile;
}
export function createMyQRCode(expiresInSeconds = 86400) {
return imPost<MyQRCode>('/me/qrcodes', { purpose: 'add_friend', expires_in_seconds: expiresInSeconds });
}
/** `identifier`: user_id/username/public_account -- viewing a specific friend's own QR card. */
export function createUserQRCode(identifier: string, expiresInSeconds = 86400) {
return imPost<MyQRCode>(`/users/${encodeURIComponent(identifier)}/qrcodes`, { purpose: 'add_friend', expires_in_seconds: expiresInSeconds });
}
export interface QRResolveUser {
user_id: string;
display_name: string;
public_account?: string;
// docs/IM-前后端对接文档.md:1289 -- same real direction-specific enum as friends.ts's
// UserSearchResult/UserProfile, never a bare "pending".
relationship: 'none' | 'friend' | 'pending_inbound' | 'pending_outbound' | string;
can_add: boolean;
action: string;
}
/** Confirmed LIVE (2026-09-08, same account that minted the code -- a real cross-account "scan a
* stranger's group code" resolve hasn't been exercised, this session only had one account free):
* a `purpose:"join_group"` resolve nests the group under `group` (NOT `group_snapshot`, despite
* the mint response using that key -- resolve and mint use different field names for the same
* shape), and carries a `status` this specific case's doc example (line 1737, mint-only) never
* showed: resolving your own group's code (i.e. already a member) comes back `status:"joined"`.
* What a genuinely-not-yet-a-member resolve returns for `status` is unconfirmed -- treated as
* "anything other than 'joined'" in QRScannerModal/AddFriendModal, i.e. the general case that still
* needs a real join-request submission. */
export interface QRResolveGroup {
topic: string;
name: string;
member_count: number;
}
export interface QRResolveResult {
purpose: 'add_friend' | 'join_group' | string;
source: string;
source_topic: string;
expires_at: string;
user?: QRResolveUser;
group?: QRResolveGroup;
group_snapshot?: QRResolveGroup;
/** join_group only -- `"joined"` means the scanning account is already a member (confirmed live);
* any other value (including absent) is treated as "not a member yet, submit a join request". */
status?: string;
}
export function resolveQRCode(token: string, purpose: 'add_friend' | 'join_group' | string = 'add_friend') {
return imPost<QRResolveResult>('/qrcodes/resolve', { token, purpose });
}
/** docs/IM-前后端对接文档.md:1590 -- 群管理员/群主展示的入群二维码 (mirrors createMyQRCode/
* createUserQRCode's shape, just scoped to a group and carrying `group_snapshot` instead of a
* personal `display_profile`). */
export interface GroupQRCode {
qr_token: string;
qr_payload: string;
expires_at: string;
group_snapshot: QRResolveGroup;
}
export function createGroupQRCode(topic: string, expiresInSeconds = 86400) {
return imPost<GroupQRCode>(`/groups/${encodeURIComponent(topic)}/qrcodes`, { expires_in_seconds: expiresInSeconds });
}