157 lines
4.4 KiB
TypeScript
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],
|
|
);
|
|
}
|
|
}
|