Google 계정 연동 관리 개선

This commit is contained in:
2026-07-29 14:18:40 +09:00
parent 52ed053876
commit 3c431576b7
13 changed files with 258 additions and 16 deletions

View File

@@ -4,6 +4,7 @@ import { createError, getQuery, sendRedirect } from 'h3'
import {
createGoogleUser,
getUserByEmail,
getUserById,
getUserByGoogleSub,
isPrivilegedMemberSession,
linkGoogleIdentityToUser,
@@ -123,6 +124,17 @@ export default defineEventHandler(async (event) => {
return sendRedirect(event, appendGoogleStatus('/signin', 'session-mismatch'), 302)
}
const currentUser = await getUserById(session.userId)
if (!currentUser) {
clearMemberSession(event)
clearAdminSession(event)
return sendRedirect(event, appendGoogleStatus('/signin', 'session-mismatch'), 302)
}
if (currentUser.googleLinkedAt) {
return sendRedirect(event, appendGoogleStatus(state.redirectTo, 'already-connected'), 302)
}
if (linkedUser && String(linkedUser.id) !== String(session.userId)) {
return sendRedirect(event, appendGoogleStatus(state.redirectTo, 'already-linked'), 302)
}

View File

@@ -115,6 +115,10 @@ const mapAdminMemberRow = (row) => {
lastSeenIp: row.lastSeenIp || '',
previousLastSeenAt,
previousLastSeenIp: row.previousLastSeenIp || '',
googleEmail: row.googleEmail || '',
googleName: row.googleName || '',
googleAvatarUrl: row.googleAvatarUrl || '',
googleLinkedAt: row.googleLinkedAt ? row.googleLinkedAt.toISOString() : null,
commentCount: Number(row.commentCount || 0),
activityStatus: isActive ? '활성' : '비활성',
role: getMemberRoleLabel(roleCode)
@@ -826,6 +830,10 @@ export const listMembersForAdmin = async () => {
users.member_note AS "note",
users.is_admin AS "isAdmin",
users.user_role AS "roleCode",
users.google_email AS "googleEmail",
users.google_name AS "googleName",
users.google_avatar_url AS "googleAvatarUrl",
users.google_linked_at AS "googleLinkedAt",
users.created_at AS "createdAt",
users.updated_at AS "updatedAt",
users.last_seen_at AS "lastSeenAt",
@@ -859,6 +867,10 @@ export const getMemberForAdmin = async (memberId) => {
users.member_note AS "note",
users.is_admin AS "isAdmin",
users.user_role AS "roleCode",
users.google_email AS "googleEmail",
users.google_name AS "googleName",
users.google_avatar_url AS "googleAvatarUrl",
users.google_linked_at AS "googleLinkedAt",
users.created_at AS "createdAt",
users.updated_at AS "updatedAt",
users.last_seen_at AS "lastSeenAt",
@@ -946,6 +958,45 @@ export const updateMemberByAdmin = async (input) => {
return rows?.[0] ? getMemberForAdmin(rows[0].id) : null
}
/**
* 소유자 권한으로 회원 Google 계정 연결을 해제한다.
* @param {{ actorUserId: string, memberId: string }} input - 해제 정보
* @returns {Promise<Object | null>} 수정된 회원
*/
export const unlinkGoogleIdentityByOwner = async (input) => {
const actor = await getMemberForAdmin(input.actorUserId)
if (!actor || actor.roleCode !== MEMBER_ROLE.OWNER) {
throw createError({
statusCode: 403,
message: 'Google 연동 해제는 소유자만 할 수 있습니다.'
})
}
const target = await getMemberForAdmin(input.memberId)
if (!target) {
throw createError({
statusCode: 404,
message: '회원을 찾을 수 없습니다.'
})
}
const sql = requireSql()
const rows = await sql`
UPDATE users
SET
google_sub = NULL,
google_email = NULL,
google_name = NULL,
google_avatar_url = NULL,
google_linked_at = NULL,
updated_at = now()
WHERE id = ${input.memberId}
RETURNING id
`
return rows?.[0] ? getMemberForAdmin(rows[0].id) : null
}
/**
* 관리자 화면에서 회원 썸네일만 수정한다.
* @param {{ actorUserId: string, memberId: string, avatarUrl: string }} input - 수정 값

View File

@@ -0,0 +1,34 @@
import { createError, getRouterParam } from 'h3'
import { requireAdminSession } from '../../../../../utils/admin-auth'
import { unlinkGoogleIdentityByOwner } from '../../../../../repositories/member-repository'
/**
* 관리자 회원 Google 계정 연결 해제 API
* @param {import('h3').H3Event} event - 요청 이벤트
* @returns {Promise<Object>} 수정된 회원
*/
export default defineEventHandler(async (event) => {
const session = requireAdminSession(event)
const memberId = String(getRouterParam(event, 'id') || '')
if (!memberId) {
throw createError({
statusCode: 400,
message: '회원 ID가 필요합니다.'
})
}
const updated = await unlinkGoogleIdentityByOwner({
actorUserId: session.userId,
memberId
})
if (!updated) {
throw createError({
statusCode: 500,
message: 'Google 연동 해제에 실패했습니다.'
})
}
return updated
})