feat(gateway): recover orphaned Kakao account links
This commit is contained in:
@@ -161,24 +161,58 @@ export const createInMemoryUserRepository = (hasher: PasswordHasher = createSimp
|
||||
throw new Error('User not found.');
|
||||
},
|
||||
async linkKakao(userId, input): Promise<UserRecord> {
|
||||
if (usersByOauthId.has(`KAKAO:${input.oauthId}`) || usersByEmail.has(input.email.toLowerCase())) {
|
||||
const normalizedEmail = input.email.toLowerCase();
|
||||
const oauthOwner = usersByOauthId.get(`KAKAO:${input.oauthId}`);
|
||||
const emailOwner = usersByEmail.get(normalizedEmail);
|
||||
if ((oauthOwner && oauthOwner.id !== userId) || (emailOwner && emailOwner.id !== userId)) {
|
||||
throw new Error('Kakao account already linked.');
|
||||
}
|
||||
for (const user of usersByName.values()) {
|
||||
if (user.id !== userId) {
|
||||
continue;
|
||||
}
|
||||
if (user.oauthType === 'KAKAO' && user.oauthId) {
|
||||
usersByOauthId.delete(`KAKAO:${user.oauthId}`);
|
||||
}
|
||||
if (user.email && user.email.toLowerCase() !== normalizedEmail) {
|
||||
usersByEmail.delete(user.email.toLowerCase());
|
||||
}
|
||||
user.oauthType = 'KAKAO';
|
||||
user.oauthId = input.oauthId;
|
||||
user.email = input.email.toLowerCase();
|
||||
user.email = normalizedEmail;
|
||||
user.oauthInfo = input.oauthInfo;
|
||||
user.kakaoVerifiedAt = input.verifiedAt.toISOString();
|
||||
user.kakaoTalkVerifiedUntil = undefined;
|
||||
usersByOauthId.set(`KAKAO:${input.oauthId}`, user);
|
||||
usersByEmail.set(user.email, user);
|
||||
return user;
|
||||
}
|
||||
throw new Error('User not found.');
|
||||
},
|
||||
async relinkKakaoByEmail(userId, input): Promise<UserRecord> {
|
||||
const normalizedEmail = input.email.toLowerCase();
|
||||
const oauthOwner = usersByOauthId.get(`KAKAO:${input.oauthId}`);
|
||||
const emailOwner = usersByEmail.get(normalizedEmail);
|
||||
if ((oauthOwner && oauthOwner.id !== userId) || emailOwner?.id !== userId) {
|
||||
throw new Error('Kakao account recovery ownership changed.');
|
||||
}
|
||||
for (const user of usersByName.values()) {
|
||||
if (user.id !== userId || user.email?.toLowerCase() !== normalizedEmail) {
|
||||
continue;
|
||||
}
|
||||
if (user.oauthType === 'KAKAO' && user.oauthId) {
|
||||
usersByOauthId.delete(`KAKAO:${user.oauthId}`);
|
||||
}
|
||||
user.oauthType = 'KAKAO';
|
||||
user.oauthId = input.oauthId;
|
||||
user.oauthInfo = input.oauthInfo;
|
||||
user.kakaoVerifiedAt = input.verifiedAt.toISOString();
|
||||
user.kakaoTalkVerifiedUntil = undefined;
|
||||
usersByOauthId.set(`KAKAO:${input.oauthId}`, user);
|
||||
return user;
|
||||
}
|
||||
throw new Error('Kakao account recovery ownership changed.');
|
||||
},
|
||||
async updateRoles(userId: string, roles: string[]): Promise<void> {
|
||||
for (const user of usersByName.values()) {
|
||||
if (user.id === userId) {
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface OAuthPendingState {
|
||||
export interface OAuthSession {
|
||||
id: string;
|
||||
mode: OAuthMode;
|
||||
intent?: 'register' | 'link_existing' | 'rejoin';
|
||||
targetUserId?: string;
|
||||
kakaoId: string;
|
||||
email: string;
|
||||
accessToken: string;
|
||||
|
||||
@@ -251,10 +251,32 @@ export const createPostgresUserRepository = (
|
||||
email: input.email.toLowerCase(),
|
||||
oauthInfo: input.oauthInfo as GatewayPrisma.JsonObject,
|
||||
kakaoVerifiedAt: input.verifiedAt,
|
||||
kakaoTalkVerifiedUntil: null,
|
||||
},
|
||||
});
|
||||
return mapUser(row);
|
||||
},
|
||||
async relinkKakaoByEmail(userId, input): Promise<UserRecord> {
|
||||
const normalizedEmail = input.email.toLowerCase();
|
||||
const updated = await prisma.appUser.updateMany({
|
||||
where: {
|
||||
id: userId,
|
||||
email: normalizedEmail,
|
||||
},
|
||||
data: {
|
||||
oauthType: 'KAKAO',
|
||||
oauthId: input.oauthId,
|
||||
oauthInfo: input.oauthInfo as GatewayPrisma.JsonObject,
|
||||
kakaoVerifiedAt: input.verifiedAt,
|
||||
kakaoTalkVerifiedUntil: null,
|
||||
},
|
||||
});
|
||||
if (updated.count !== 1) {
|
||||
throw new Error('Kakao account recovery ownership changed.');
|
||||
}
|
||||
const row = await prisma.appUser.findUniqueOrThrow({ where: { id: userId } });
|
||||
return mapUser(row);
|
||||
},
|
||||
async updateRoles(userId: string, roles: string[]): Promise<void> {
|
||||
await prisma.appUser.update({
|
||||
where: { id: userId },
|
||||
|
||||
@@ -122,6 +122,15 @@ export interface UserRepository {
|
||||
verifiedAt: Date;
|
||||
}
|
||||
): Promise<UserRecord>;
|
||||
relinkKakaoByEmail(
|
||||
userId: string,
|
||||
input: {
|
||||
oauthId: string;
|
||||
email: string;
|
||||
oauthInfo: UserOAuthInfo;
|
||||
verifiedAt: Date;
|
||||
}
|
||||
): Promise<UserRecord>;
|
||||
updateRoles(userId: string, roles: string[]): Promise<void>;
|
||||
updateSanctions(userId: string, sanctions: UserSanctions): Promise<void>;
|
||||
updateKakaoGraceUntil(userId: string, until: Date | null): Promise<void>;
|
||||
|
||||
Reference in New Issue
Block a user