Files
Genarrative/server-node/src/repositories/authIdentityRepository.ts
2026-04-10 15:37:02 +08:00

157 lines
4.4 KiB
TypeScript

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<string, unknown> | 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<string, unknown> | 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<string, unknown> | 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<AuthIdentityRow>(
`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<AuthIdentityRow>(
`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<AuthIdentityRow>(
`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<AuthIdentityRow>(
`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],
);
}
}