import crypto from 'node:crypto'; import type { QueryResultRow } from 'pg'; import type { AppDatabase } from '../db.js'; export type AuthIdentityProvider = 'wechat'; export type AuthIdentityRecord = { id: string; userId: string; provider: AuthIdentityProvider; providerUid: string; providerUnionId: string | null; displayName: string | null; avatarUrl: string | null; isVerified: boolean; metaJson: Record | null; createdAt: string; updatedAt: string; }; type AuthIdentityRow = QueryResultRow & { id: string; user_id: string; provider: AuthIdentityProvider; provider_uid: string; provider_unionid: string | null; display_name: string | null; avatar_url: string | null; is_verified: boolean; meta_json: Record | null; created_at: string; updated_at: string; }; function toAuthIdentityRecord( row: AuthIdentityRow | undefined, ): AuthIdentityRecord | null { if (!row) { return null; } return { id: row.id, userId: row.user_id, provider: row.provider, providerUid: row.provider_uid, providerUnionId: row.provider_unionid, displayName: row.display_name, avatarUrl: row.avatar_url, isVerified: row.is_verified, metaJson: row.meta_json, createdAt: row.created_at, updatedAt: row.updated_at, }; } export type CreateWechatIdentityInput = { userId: string; providerUid: string; providerUnionId: string | null; displayName: string | null; avatarUrl: string | null; metaJson?: Record | null; }; export class AuthIdentityRepository { constructor(private readonly db: AppDatabase) {} async findWechatIdentityByProfile(params: { providerUid: string; providerUnionId: string | null; }) { const result = params.providerUnionId ? await this.db.query( `SELECT id, user_id, provider, provider_uid, provider_unionid, display_name, avatar_url, is_verified, meta_json, created_at, updated_at FROM auth_identities WHERE provider = 'wechat' AND (provider_unionid = $1 OR provider_uid = $2) ORDER BY CASE WHEN provider_unionid = $1 THEN 0 ELSE 1 END LIMIT 1`, [params.providerUnionId, params.providerUid], ) : await this.db.query( `SELECT id, user_id, provider, provider_uid, provider_unionid, display_name, avatar_url, is_verified, meta_json, created_at, updated_at FROM auth_identities WHERE provider = 'wechat' AND provider_uid = $1 LIMIT 1`, [params.providerUid], ); return toAuthIdentityRecord(result.rows[0]); } async listByUserId(userId: string) { const result = await this.db.query( `SELECT id, user_id, provider, provider_uid, provider_unionid, display_name, avatar_url, is_verified, meta_json, created_at, updated_at FROM auth_identities WHERE user_id = $1 ORDER BY provider, created_at`, [userId], ); return result.rows .map((row) => toAuthIdentityRecord(row)) .filter((row): row is AuthIdentityRecord => Boolean(row)); } async createWechatIdentity(input: CreateWechatIdentityInput) { const now = new Date().toISOString(); const identityId = `authi_${crypto.randomBytes(16).toString('hex')}`; const result = await this.db.query( `INSERT INTO auth_identities ( id, user_id, provider, provider_uid, provider_unionid, display_name, avatar_url, is_verified, meta_json, created_at, updated_at ) VALUES ($1, $2, 'wechat', $3, $4, $5, $6, TRUE, $7, $8, $9) RETURNING id, user_id, provider, provider_uid, provider_unionid, display_name, avatar_url, is_verified, meta_json, created_at, updated_at`, [ identityId, input.userId, input.providerUid, input.providerUnionId, input.displayName, input.avatarUrl, input.metaJson ?? null, now, now, ], ); return toAuthIdentityRecord(result.rows[0]); } async moveWechatIdentitiesToUser(sourceUserId: string, targetUserId: string) { await this.db.query( `UPDATE auth_identities SET user_id = $1, updated_at = $2 WHERE user_id = $3 AND provider = 'wechat'`, [targetUserId, new Date().toISOString(), sourceUserId], ); } }