social 계정 연동 refresh 기능 추가 .
parent
89ea0c783e
commit
325fb9af69
|
|
@ -123,6 +123,7 @@ class TokenExpiredError(OAuthException):
|
|||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
code="TOKEN_EXPIRED",
|
||||
)
|
||||
self.platform = platform
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
|
|
|||
|
|
@ -27,8 +27,10 @@ redis_client = Redis(
|
|||
from app.social.exceptions import (
|
||||
InvalidStateError,
|
||||
OAuthStateExpiredError,
|
||||
OAuthTokenRefreshError,
|
||||
SocialAccountAlreadyConnectedError,
|
||||
SocialAccountNotFoundError,
|
||||
TokenExpiredError,
|
||||
)
|
||||
from app.social.oauth import get_oauth_client
|
||||
from app.social.schemas import (
|
||||
|
|
@ -209,13 +211,15 @@ class SocialAccountService:
|
|||
self,
|
||||
user_uuid: str,
|
||||
session: AsyncSession,
|
||||
auto_refresh: bool = True,
|
||||
) -> list[SocialAccountResponse]:
|
||||
"""
|
||||
연동된 소셜 계정 목록 조회
|
||||
연동된 소셜 계정 목록 조회 (토큰 자동 갱신 포함)
|
||||
|
||||
Args:
|
||||
user_uuid: 사용자 UUID
|
||||
session: DB 세션
|
||||
auto_refresh: 토큰 자동 갱신 여부 (기본 True)
|
||||
|
||||
Returns:
|
||||
list[SocialAccountResponse]: 연동된 계정 목록
|
||||
|
|
@ -233,8 +237,95 @@ class SocialAccountService:
|
|||
|
||||
logger.debug(f"[SOCIAL] 연동 계정 {len(accounts)}개 조회됨")
|
||||
|
||||
# 토큰 자동 갱신
|
||||
if auto_refresh:
|
||||
for account in accounts:
|
||||
await self._try_refresh_token(account, session)
|
||||
|
||||
return [self._to_response(account) for account in accounts]
|
||||
|
||||
async def refresh_all_tokens(
|
||||
self,
|
||||
user_uuid: str,
|
||||
session: AsyncSession,
|
||||
) -> dict[str, bool]:
|
||||
"""
|
||||
사용자의 모든 연동 계정 토큰 갱신 (로그인 시 호출)
|
||||
|
||||
Args:
|
||||
user_uuid: 사용자 UUID
|
||||
session: DB 세션
|
||||
|
||||
Returns:
|
||||
dict[str, bool]: 플랫폼별 갱신 성공 여부
|
||||
"""
|
||||
logger.info(f"[SOCIAL] 모든 연동 계정 토큰 갱신 시작 - user_uuid: {user_uuid}")
|
||||
|
||||
result = await session.execute(
|
||||
select(SocialAccount).where(
|
||||
SocialAccount.user_uuid == user_uuid,
|
||||
SocialAccount.is_active == True, # noqa: E712
|
||||
SocialAccount.is_deleted == False, # noqa: E712
|
||||
)
|
||||
)
|
||||
accounts = result.scalars().all()
|
||||
|
||||
refresh_results = {}
|
||||
for account in accounts:
|
||||
success = await self._try_refresh_token(account, session)
|
||||
refresh_results[f"{account.platform}_{account.id}"] = success
|
||||
|
||||
logger.info(f"[SOCIAL] 토큰 갱신 완료 - results: {refresh_results}")
|
||||
return refresh_results
|
||||
|
||||
async def _try_refresh_token(
|
||||
self,
|
||||
account: SocialAccount,
|
||||
session: AsyncSession,
|
||||
) -> bool:
|
||||
"""
|
||||
토큰 갱신 시도 (실패해도 예외 발생하지 않음)
|
||||
|
||||
Args:
|
||||
account: 소셜 계정
|
||||
session: DB 세션
|
||||
|
||||
Returns:
|
||||
bool: 갱신 성공 여부
|
||||
"""
|
||||
# refresh_token이 없으면 갱신 불가
|
||||
if not account.refresh_token:
|
||||
logger.debug(
|
||||
f"[SOCIAL] refresh_token 없음, 갱신 스킵 - account_id: {account.id}"
|
||||
)
|
||||
return False
|
||||
|
||||
# 만료 시간 확인 (만료 1시간 전이면 갱신)
|
||||
should_refresh = False
|
||||
if account.token_expires_at is None:
|
||||
should_refresh = True
|
||||
else:
|
||||
buffer_time = datetime.now() + timedelta(hours=1)
|
||||
if account.token_expires_at <= buffer_time:
|
||||
should_refresh = True
|
||||
|
||||
if not should_refresh:
|
||||
logger.debug(
|
||||
f"[SOCIAL] 토큰 아직 유효, 갱신 스킵 - account_id: {account.id}"
|
||||
)
|
||||
return True
|
||||
|
||||
# 갱신 시도
|
||||
try:
|
||||
await self._refresh_account_token(account, session)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"[SOCIAL] 토큰 갱신 실패 (재연동 필요) - "
|
||||
f"account_id: {account.id}, error: {e}"
|
||||
)
|
||||
return False
|
||||
|
||||
async def get_account_by_platform(
|
||||
self,
|
||||
user_uuid: str,
|
||||
|
|
@ -402,14 +493,33 @@ class SocialAccountService:
|
|||
|
||||
Returns:
|
||||
str: 유효한 access_token
|
||||
|
||||
Raises:
|
||||
TokenExpiredError: 토큰 갱신 실패 시 (재연동 필요)
|
||||
"""
|
||||
# 만료 시간 확인 (만료 10분 전이면 갱신)
|
||||
if account.token_expires_at:
|
||||
# refresh_token이 없으면 갱신 불가 → 재연동 필요
|
||||
if not account.refresh_token:
|
||||
logger.warning(
|
||||
f"[SOCIAL] refresh_token 없음, 재연동 필요 - account_id: {account.id}"
|
||||
)
|
||||
raise TokenExpiredError(platform=account.platform)
|
||||
|
||||
# 만료 시간 확인 (만료 10분 전이면 갱신, 만료 시간 없어도 갱신 시도)
|
||||
should_refresh = False
|
||||
if account.token_expires_at is None:
|
||||
logger.info(
|
||||
f"[SOCIAL] token_expires_at 없음, 갱신 시도 - account_id: {account.id}"
|
||||
)
|
||||
should_refresh = True
|
||||
else:
|
||||
buffer_time = datetime.now() + timedelta(minutes=10)
|
||||
if account.token_expires_at <= buffer_time:
|
||||
logger.info(
|
||||
f"[SOCIAL] 토큰 만료 임박, 갱신 시작 - account_id: {account.id}"
|
||||
)
|
||||
should_refresh = True
|
||||
|
||||
if should_refresh:
|
||||
return await self._refresh_account_token(account, session)
|
||||
|
||||
return account.access_token
|
||||
|
|
@ -428,17 +538,33 @@ class SocialAccountService:
|
|||
|
||||
Returns:
|
||||
str: 새 access_token
|
||||
|
||||
Raises:
|
||||
TokenExpiredError: 갱신 실패 시 (재연동 필요)
|
||||
"""
|
||||
if not account.refresh_token:
|
||||
logger.warning(
|
||||
f"[SOCIAL] refresh_token 없음, 갱신 불가 - account_id: {account.id}"
|
||||
f"[SOCIAL] refresh_token 없음, 재연동 필요 - account_id: {account.id}"
|
||||
)
|
||||
return account.access_token
|
||||
raise TokenExpiredError(platform=account.platform)
|
||||
|
||||
platform = SocialPlatform(account.platform)
|
||||
oauth_client = get_oauth_client(platform)
|
||||
|
||||
try:
|
||||
token_response = await oauth_client.refresh_token(account.refresh_token)
|
||||
except OAuthTokenRefreshError as e:
|
||||
logger.error(
|
||||
f"[SOCIAL] 토큰 갱신 실패, 재연동 필요 - "
|
||||
f"account_id: {account.id}, error: {e}"
|
||||
)
|
||||
raise TokenExpiredError(platform=account.platform)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[SOCIAL] 토큰 갱신 중 예외 발생, 재연동 필요 - "
|
||||
f"account_id: {account.id}, error: {e}"
|
||||
)
|
||||
raise TokenExpiredError(platform=account.platform)
|
||||
|
||||
# 토큰 업데이트
|
||||
account.access_token = token_response.access_token
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from sqlalchemy.exc import SQLAlchemyError
|
|||
from config import social_upload_settings
|
||||
from app.database.session import BackgroundSessionLocal
|
||||
from app.social.constants import SocialPlatform, UploadStatus
|
||||
from app.social.exceptions import UploadError, UploadQuotaExceededError
|
||||
from app.social.exceptions import TokenExpiredError, UploadError, UploadQuotaExceededError
|
||||
from app.social.models import SocialUpload
|
||||
from app.social.services import social_account_service
|
||||
from app.social.uploader import get_uploader
|
||||
|
|
@ -352,6 +352,17 @@ async def process_social_upload(upload_id: int) -> None:
|
|||
error_message="플랫폼 API 일일 할당량이 초과되었습니다. 내일 다시 시도해주세요.",
|
||||
)
|
||||
|
||||
except TokenExpiredError as e:
|
||||
logger.error(
|
||||
f"[SOCIAL_UPLOAD] 토큰 만료, 재연동 필요 - "
|
||||
f"upload_id: {upload_id}, platform: {e.platform}"
|
||||
)
|
||||
await _update_upload_status(
|
||||
upload_id=upload_id,
|
||||
status=UploadStatus.FAILED,
|
||||
error_message=f"{e.platform} 계정 인증이 만료되었습니다. 계정을 다시 연동해주세요.",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[SOCIAL_UPLOAD] 예상치 못한 에러 - "
|
||||
|
|
|
|||
|
|
@ -33,10 +33,12 @@ from app.user.services import auth_service, kakao_client
|
|||
from app.user.services.jwt import (
|
||||
create_access_token,
|
||||
create_refresh_token,
|
||||
decode_token,
|
||||
get_access_token_expire_seconds,
|
||||
get_refresh_token_expires_at,
|
||||
get_token_hash,
|
||||
)
|
||||
from app.social.services import social_account_service
|
||||
from app.utils.common import generate_uuid
|
||||
|
||||
|
||||
|
|
@ -140,6 +142,19 @@ async def kakao_callback(
|
|||
ip_address=ip_address,
|
||||
)
|
||||
|
||||
# 로그인 성공 후 연동된 소셜 계정 토큰 자동 갱신
|
||||
try:
|
||||
payload = decode_token(result.access_token)
|
||||
if payload and payload.get("sub"):
|
||||
user_uuid = payload.get("sub")
|
||||
await social_account_service.refresh_all_tokens(
|
||||
user_uuid=user_uuid,
|
||||
session=session,
|
||||
)
|
||||
except Exception as e:
|
||||
# 토큰 갱신 실패해도 로그인은 성공 처리
|
||||
logger.warning(f"[ROUTER] 소셜 계정 토큰 갱신 실패 (무시) - error: {e}")
|
||||
|
||||
# 프론트엔드로 토큰과 함께 리다이렉트
|
||||
redirect_url = (
|
||||
f"{prj_settings.PROJECT_DOMAIN}"
|
||||
|
|
@ -205,9 +220,20 @@ async def kakao_verify(
|
|||
ip_address=ip_address,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[ROUTER] 카카오 인가 코드 검증 완료 - user_id: {result.user.id}, is_new_user: {result.user.is_new_user}"
|
||||
# 로그인 성공 후 연동된 소셜 계정 토큰 자동 갱신
|
||||
try:
|
||||
payload = decode_token(result.access_token)
|
||||
if payload and payload.get("sub"):
|
||||
user_uuid = payload.get("sub")
|
||||
await social_account_service.refresh_all_tokens(
|
||||
user_uuid=user_uuid,
|
||||
session=session,
|
||||
)
|
||||
except Exception as e:
|
||||
# 토큰 갱신 실패해도 로그인은 성공 처리
|
||||
logger.warning(f"[ROUTER] 소셜 계정 토큰 갱신 실패 (무시) - error: {e}")
|
||||
|
||||
logger.info(f"[ROUTER] 카카오 인가 코드 검증 완료 - is_new_user: {result.is_new_user}")
|
||||
return result
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue