diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index 68bc01d936..ec5e524c67 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -2,3 +2,6 @@ export * from './regex' /** API 호출 시 필요한 사용자 세션 HTTP 헤더 키 */ export const SESSION_KEY = 'x-soto-session' +export const TP_TK = 'TP_TK' +export const TP_SE = 'TP_SE' +export const X_TRIPLE_WEB_DEVICE_ID = 'x-triple-web-device-id' diff --git a/packages/public-header/src/side-menu/profile.tsx b/packages/public-header/src/side-menu/profile.tsx index 783882421e..fa3f1a7690 100644 --- a/packages/public-header/src/side-menu/profile.tsx +++ b/packages/public-header/src/side-menu/profile.tsx @@ -73,6 +73,8 @@ const PROFILE_EVENT_METADATA_LABEL = { photo: '프로필사진', } +const NOL_CONNECTED_LABEL = 'NOL 멤버스 계정' + export function Profile() { const user = useUser() const { trackEvent } = useEventTrackingContext() @@ -80,6 +82,10 @@ export function Profile() { const returnUrl = encodeURIComponent(location.href) const providerIconSrc = user ? PROVIDER_INFO[user.provider].icon : undefined const badgeUrl = user ? user.mileage?.badges[0]?.icon.image_url : undefined + const providerVisible = user && !user.nolConnected + const profileLabel = providerVisible + ? user.email || PROVIDER_INFO[user.provider].label + : NOL_CONNECTED_LABEL const onProfileClick = ( referrer: keyof typeof PROFILE_EVENT_METADATA_LABEL, @@ -105,10 +111,10 @@ export function Profile() { <Container> <UserName onClick={() => onProfileClick('name')}>{user.name}</UserName> <UserEmailOrProvider> - {providerIconSrc ? ( + {providerIconSrc && providerVisible ? ( <SocialIcon src={providerIconSrc} alt="social login icon" /> ) : null} - {user.email || PROVIDER_INFO[user.provider].label} + {profileLabel} </UserEmailOrProvider> </Container> diff --git a/packages/react-contexts/package.json b/packages/react-contexts/package.json index a33e5306e0..712f686d3e 100644 --- a/packages/react-contexts/package.json +++ b/packages/react-contexts/package.json @@ -72,7 +72,8 @@ "semver": "^7.5.4", "set-cookie-parser": "^2.7.1", "ua-parser-js": "^1.0.37", - "universal-cookie": "^4.0.4" + "universal-cookie": "^4.0.4", + "uuid": "^11.1.0" }, "devDependencies": { "@types/qs": "^6.9.9", diff --git a/packages/react-contexts/src/middlewares/chain.ts b/packages/react-contexts/src/middlewares/chain.ts index 80c03ff054..9f89425d92 100644 --- a/packages/react-contexts/src/middlewares/chain.ts +++ b/packages/react-contexts/src/middlewares/chain.ts @@ -1,16 +1,11 @@ -import { NextResponse } from 'next/server' -import type { NextFetchEvent, NextRequest } from 'next/server' +import { NextMiddleware, NextResponse } from 'next/server' -import { CustomMiddleware, MiddlewareFactory } from './types' +import { MiddlewareFactory } from './types' -/** - * TF 14.0.11의 middlewares/src/chain 참고하여 작성 - * https://github.com/titicacadev/triple-frontend/blob/ceee9a7116dfbf39cc1363013a43cfb0060a9539/packages/middlewares/src/chain.ts - */ export function chain( functions: MiddlewareFactory[], index = 0, -): CustomMiddleware { +): NextMiddleware { const current = functions[index] if (current) { @@ -18,11 +13,7 @@ export function chain( return current(next) } - return ( - request: NextRequest, - event: NextFetchEvent, - response: NextResponse, - ) => { - return response + return () => { + return NextResponse.next() } } diff --git a/packages/react-contexts/src/middlewares/constants.ts b/packages/react-contexts/src/middlewares/constants.ts deleted file mode 100644 index 1203e84c5a..0000000000 --- a/packages/react-contexts/src/middlewares/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const TP_TK = 'TP_TK' -export const TP_SE = 'TP_SE' diff --git a/packages/react-contexts/src/middlewares/index.ts b/packages/react-contexts/src/middlewares/index.ts index bb364a75e9..6ff8e38c41 100644 --- a/packages/react-contexts/src/middlewares/index.ts +++ b/packages/react-contexts/src/middlewares/index.ts @@ -1,10 +1,9 @@ import { chain } from './chain' import type { MiddlewareFactory } from './types' import { refreshSessionMiddleware } from './refresh-session' - -export { oldTripleIosCookiesMiddleware } from './old-triple-ios-cookie' +import { setWebDeviceIdMiddleware } from './set-web-device-id' export const constructMiddleware = (functions: MiddlewareFactory[]) => - chain([...functions, refreshSessionMiddleware]) + chain([...functions, refreshSessionMiddleware, setWebDeviceIdMiddleware]) export * from './types' diff --git a/packages/react-contexts/src/middlewares/old-triple-ios-cookie.ts b/packages/react-contexts/src/middlewares/old-triple-ios-cookie.ts deleted file mode 100644 index ce7c7bbf92..0000000000 --- a/packages/react-contexts/src/middlewares/old-triple-ios-cookie.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { NextRequest, NextResponse, NextFetchEvent } from 'next/server' -import satisfies from 'semver/functions/satisfies' - -import { parseApp } from '../user-agent-context' - -import { CustomMiddleware } from './types' - -/** - * TF 13.42.1의 react-contexts/src/middleware 참고하여 작성 - * https://github.com/titicacadev/triple-frontend/blob/8d002e80f9ff187d6a06b6a2695c48f1d5383662/packages/react-contexts/src/middleware.ts - */ -export function oldTripleIosCookiesMiddleware( - customMiddleware: CustomMiddleware, -) { - return function middleware(request: NextRequest, event: NextFetchEvent) { - const response = NextResponse.next() - - const userAgent = request.headers.get('User-Agent') - - const host = request.headers.get('x-forwarded-host') - - const tripleApp = userAgent ? parseApp(userAgent) : null - - if (!userAgent || (host && !!tripleApp)) { - return customMiddleware(request, event, response) - } - - try { - const oldVersionRange = '< 6.5.5' - const isOldIosApp = - tripleApp && - tripleApp.name === 'Triple-iOS' && - satisfies(tripleApp.version, oldVersionRange, { - includePrerelease: true, - }) - - if (isOldIosApp) { - const cookies = request.cookies.getAll() - - cookies.forEach((cookie) => { - response.cookies.set(cookie.name, cookie.value) - }) - } - } catch { - // semver 파싱 에러가 발생하면 ignore 합니다. - } - - return customMiddleware(request, event, response) - } -} diff --git a/packages/react-contexts/src/middlewares/refresh-session.ts b/packages/react-contexts/src/middlewares/refresh-session.ts index 6edc005750..2154c200ce 100644 --- a/packages/react-contexts/src/middlewares/refresh-session.ts +++ b/packages/react-contexts/src/middlewares/refresh-session.ts @@ -1,31 +1,51 @@ -import { NextFetchEvent, NextRequest, NextResponse } from 'next/server' +import { type ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies' +import { + NextFetchEvent, + NextMiddleware, + NextRequest, + NextResponse, +} from 'next/server' import { get, post } from '@titicaca/fetcher' import { parseString, splitCookiesString } from 'set-cookie-parser' +import { + TP_SE, + TP_TK, + SESSION_KEY as X_SOTO_SESSION, +} from '@titicaca/constants' -import { CustomMiddleware } from './types' -import { TP_SE, TP_TK } from './constants' +import { parseApp } from '../user-agent-context' -export function refreshSessionMiddleware(customMiddleware: CustomMiddleware) { +import { applySetCookie } from './utils/apply-set-cookie' + +export function refreshSessionMiddleware(next: NextMiddleware) { return async function middleware( request: NextRequest, event: NextFetchEvent, ) { + const response = (await next(request, event)) as NextResponse const url = request.nextUrl const isPageUrl = url.pathname.match('^/((?!(api|static|.*\\..*|_next)).*)') if (!isPageUrl) { - return customMiddleware(request, event, NextResponse.next()) + return response } const allCookies = request.cookies.getAll() - const isSessionExisted = allCookies.some( + const userAgent = request.headers.get('User-Agent') + const tripleApp = userAgent ? parseApp(userAgent) : null + + const cookiesWithoutXSotoSession = tripleApp + ? allCookies + : allCookies.filter(({ name }) => name !== X_SOTO_SESSION) + + const isSessionExisted = cookiesWithoutXSotoSession.some( ({ name }) => name === TP_TK || name === TP_SE, ) - const cookies = deriveAllCookies(request.cookies.getAll()) + const cookies = deriveAllCookies(cookiesWithoutXSotoSession) if (!isSessionExisted) { - return customMiddleware(request, event, NextResponse.next()) + return response } const options = { @@ -36,7 +56,7 @@ export function refreshSessionMiddleware(customMiddleware: CustomMiddleware) { const firstTrialResponse = await get('/api/users/me', options) if (firstTrialResponse.status !== 401) { - return customMiddleware(request, event, NextResponse.next()) + return response } /** @@ -48,35 +68,21 @@ export function refreshSessionMiddleware(customMiddleware: CustomMiddleware) { const setCookie = refreshResponse.headers.get('set-cookie') if (setCookie) { - const oldCookies = splitCookiesString( - request.headers.get('cookie') || '', - ) + const response = (await next(request, event)) as NextResponse const setCookies = splitCookiesString(setCookie) - - const newCookies = oldCookies.reduce((map, cookie) => { - const { name } = parseString(cookie) - return map.set(name, cookie) - }, new Map()) - setCookies.forEach((cookie) => { - const { name } = parseString(cookie) - newCookies.set(name, cookie) + const { name, value, ...rest } = parseString(cookie) + if (name !== X_SOTO_SESSION) { + response.cookies.set(name, value, { ...(rest as ResponseCookie) }) + } }) + applySetCookie(request, response) - const finalCookie = [...newCookies.values()].join('; ') - - request.headers.set('cookie', finalCookie) - - const response = NextResponse.next({ - request, - }) - - response.headers.set('set-cookie', setCookie) - - return customMiddleware(request, event, response) + return response } } - return customMiddleware(request, event, NextResponse.next()) + + return response } } diff --git a/packages/react-contexts/src/middlewares/set-web-device-id.ts b/packages/react-contexts/src/middlewares/set-web-device-id.ts new file mode 100644 index 0000000000..7f61995c4e --- /dev/null +++ b/packages/react-contexts/src/middlewares/set-web-device-id.ts @@ -0,0 +1,40 @@ +import { + NextFetchEvent, + NextMiddleware, + NextRequest, + NextResponse, +} from 'next/server' +import { v4 as uuidV4 } from 'uuid' +import { X_TRIPLE_WEB_DEVICE_ID } from '@titicaca/constants' + +import { getTripleApp } from './utils/get-triple-app' +import { applySetCookie } from './utils/apply-set-cookie' + +export function setWebDeviceIdMiddleware(next: NextMiddleware) { + return async function middleware( + request: NextRequest, + event: NextFetchEvent, + ) { + const response = (await next(request, event)) as NextResponse + const tripleApp = getTripleApp(request) + + if (tripleApp) { + return response + } + + const allCookies = request.cookies.getAll() + const hasWebDeviceId = allCookies.some( + ({ name }) => name === X_TRIPLE_WEB_DEVICE_ID, + ) + + if (!hasWebDeviceId && response?.cookies) { + const randomWebDeviceId = uuidV4() + response.cookies.set(X_TRIPLE_WEB_DEVICE_ID, randomWebDeviceId, { + secure: true, + }) + applySetCookie(request, response) + } + + return response + } +} diff --git a/packages/react-contexts/src/middlewares/types.ts b/packages/react-contexts/src/middlewares/types.ts index 6638b6b286..1e3a7a7e85 100644 --- a/packages/react-contexts/src/middlewares/types.ts +++ b/packages/react-contexts/src/middlewares/types.ts @@ -1,12 +1,3 @@ -import { NextMiddlewareResult } from 'next/dist/server/web/types' -import { NextFetchEvent, NextRequest, NextResponse } from 'next/server' +import { NextMiddleware } from 'next/server' -export type CustomMiddleware = ( - request: NextRequest, - event: NextFetchEvent, - response: NextResponse, -) => NextMiddlewareResult | Promise<NextMiddlewareResult> - -export type MiddlewareFactory = ( - middleware: CustomMiddleware, -) => CustomMiddleware +export type MiddlewareFactory = (middleware: NextMiddleware) => NextMiddleware diff --git a/packages/react-contexts/src/middlewares/utils/apply-set-cookie.ts b/packages/react-contexts/src/middlewares/utils/apply-set-cookie.ts new file mode 100644 index 0000000000..2088ef2274 --- /dev/null +++ b/packages/react-contexts/src/middlewares/utils/apply-set-cookie.ts @@ -0,0 +1,27 @@ +import { + RequestCookies, + ResponseCookies, +} from 'next/dist/compiled/@edge-runtime/cookies' +import { NextRequest, NextResponse } from 'next/server' + +/** Reference: https://github.com/vercel/next.js/discussions/50374#discussioncomment-6732402 + * response.cookies.set 사용 시 response의 set-cookie로 값이 적용되기 때문에 이를 response의 headers.cookie로 설정해주는 함수입니다. + * + */ +export function applySetCookie(req: NextRequest, res: NextResponse) { + const setCookies = new ResponseCookies(res.headers) + const newReqHeaders = new Headers(req.headers) + const newReqCookies = new RequestCookies(newReqHeaders) + setCookies.getAll().forEach((cookie) => newReqCookies.set(cookie)) + + const dummyRes = NextResponse.next({ request: { headers: newReqHeaders } }) + + dummyRes.headers.forEach((value, key) => { + if ( + key === 'x-middleware-override-headers' || + key.startsWith('x-middleware-request-') + ) { + res.headers.set(key, value) + } + }) +} diff --git a/packages/react-contexts/src/middlewares/utils/get-triple-app.ts b/packages/react-contexts/src/middlewares/utils/get-triple-app.ts new file mode 100644 index 0000000000..19e69efaab --- /dev/null +++ b/packages/react-contexts/src/middlewares/utils/get-triple-app.ts @@ -0,0 +1,10 @@ +import { NextRequest } from 'next/server' + +import { parseApp } from '../../user-agent-context' + +export function getTripleApp(request: NextRequest) { + const userAgent = request.headers.get('User-Agent') + const tripleApp = userAgent ? parseApp(userAgent) : null + + return tripleApp +} diff --git a/packages/react-contexts/src/session-context/browser.tsx b/packages/react-contexts/src/session-context/browser.tsx index 921f321bcb..c9797ab9e7 100644 --- a/packages/react-contexts/src/session-context/browser.tsx +++ b/packages/react-contexts/src/session-context/browser.tsx @@ -13,6 +13,7 @@ import { import { generateUrl } from '@titicaca/view-utilities' import qs from 'qs' import Cookies from 'universal-cookie' +import { TP_TK } from '@titicaca/constants' import { GET_USER_REQUEST_URL, User, UserProvider, useUserState } from './user' import { @@ -132,9 +133,5 @@ InBrowserSessionContextProvider.getInitialProps = async function ({ export function getWebSessionAvailabilityFromRequest( req: IncomingMessage | undefined, ) { - if (process.env.NODE_ENV !== 'production') { - return !!new Cookies(req?.headers.cookie).get('TP_SE') - } - - return !!req?.headers['x-triple-web-login'] + return !!new Cookies(req?.headers.cookie).get(TP_TK) } diff --git a/packages/react-contexts/src/session-context/index.tsx b/packages/react-contexts/src/session-context/index.tsx index e84d9e55a6..fc7bedef5a 100644 --- a/packages/react-contexts/src/session-context/index.tsx +++ b/packages/react-contexts/src/session-context/index.tsx @@ -1,5 +1,5 @@ export { default as SessionContextProvider } from './provider' export { useSessionAvailability, useSessionControllers } from './hooks' -export { useUser } from './user' +export { useUser, User } from './user' export { default as getSessionAvailabilityFromRequest } from './session-availability' export { putInvalidSessionIdRemover } from './invalid-session-id-remover' diff --git a/packages/react-contexts/src/session-context/session-availability.ts b/packages/react-contexts/src/session-context/session-availability.ts index b15d6ed5a4..1f4b18f6b4 100644 --- a/packages/react-contexts/src/session-context/session-availability.ts +++ b/packages/react-contexts/src/session-context/session-availability.ts @@ -1,6 +1,6 @@ import { IncomingMessage } from 'http' -import { generateUserAgentValues } from '..' +import { parseApp } from '..' import { getSessionIdFromRequest } from './app' import { getWebSessionAvailabilityFromRequest } from './browser' @@ -8,9 +8,9 @@ import { getWebSessionAvailabilityFromRequest } from './browser' export default function getSessionAvailabilityFromRequest( req: IncomingMessage | undefined, ) { - const { isPublic } = generateUserAgentValues(req?.headers['user-agent'] || '') + const app = parseApp(req?.headers['user-agent'] || '') - if (isPublic === true) { + if (!app) { return getWebSessionAvailabilityFromRequest(req) } diff --git a/packages/react-contexts/src/session-context/user.ts b/packages/react-contexts/src/session-context/user.ts index d39c426710..c85743bd94 100644 --- a/packages/react-contexts/src/session-context/user.ts +++ b/packages/react-contexts/src/session-context/user.ts @@ -10,6 +10,8 @@ export interface User { mileage: Mileage uid: string email: string + nolConnected?: boolean + nolConnectedAt?: string } type Provider = 'TRIPLE' | 'NAVER' | 'KAKAO' | 'FACEBOOK' | 'APPLE' diff --git a/packages/react-contexts/src/user-agent-context/utils.ts b/packages/react-contexts/src/user-agent-context/utils.ts index 6c414667ee..e435ab14a4 100644 --- a/packages/react-contexts/src/user-agent-context/utils.ts +++ b/packages/react-contexts/src/user-agent-context/utils.ts @@ -5,7 +5,7 @@ enum AppName { Android = 'Triple-Android', } -interface App { +export interface App { name: AppName version: string } diff --git a/packages/ui-flow/src/auth-guard/index.ts b/packages/ui-flow/src/auth-guard/index.ts index f95fbf4a2f..26045f8aec 100644 --- a/packages/ui-flow/src/auth-guard/index.ts +++ b/packages/ui-flow/src/auth-guard/index.ts @@ -3,12 +3,10 @@ import { get } from '@titicaca/fetcher' import { parseTripleClientUserAgent } from '@titicaca/react-triple-client-interfaces' import qs from 'qs' import { generateUrl, parseUrl, strictQuery } from '@titicaca/view-utilities' -import { getSessionAvailabilityFromRequest } from '@titicaca/react-contexts' - -interface UserResponse { - uid: string - // TODO -} +import { + getSessionAvailabilityFromRequest, + User, +} from '@titicaca/react-contexts' interface AuthGuardOptions { authType?: string @@ -25,7 +23,7 @@ const NON_MEMBER_REGEX = /^_PH/ export function authGuard<Props>( gssp: ( ctx: GetServerSidePropsContext & { - customContext?: { user?: UserResponse } + customContext?: { user?: User } }, ) => Promise<GetServerSidePropsResult<Props>>, options?: AuthGuardOptions, @@ -47,7 +45,7 @@ export function authGuard<Props>( ? options.resolveReturnUrl(ctx) : `${process.env.NEXT_PUBLIC_BASE_PATH || ''}${resolvedUrl}` - const response = await get<UserResponse>('/api/users/me', { + const response = await get<User>('/api/users/me', { req, retryable: true, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69a1534440..d2d7434606 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1185,6 +1185,9 @@ importers: universal-cookie: specifier: ^4.0.4 version: 4.0.4 + uuid: + specifier: ^11.1.0 + version: 11.1.0 devDependencies: '@types/qs': specifier: ^6.9.9 @@ -11591,6 +11594,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + uuid@9.0.0: resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} hasBin: true @@ -24465,6 +24472,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@11.1.0: {} + uuid@9.0.0: {} uvu@0.5.6: