diff --git a/packages/triple-web-binding-nextjs-pages/package.json b/packages/triple-web-binding-nextjs-pages/package.json index 461ba880d3..8963cd665e 100644 --- a/packages/triple-web-binding-nextjs-pages/package.json +++ b/packages/triple-web-binding-nextjs-pages/package.json @@ -39,7 +39,10 @@ ] }, "dependencies": { - "ua-parser-js": "^1.0.36" + "@titicaca/fetcher": "^13.8.1", + "@titicaca/view-utilities": "^13.8.1", + "ua-parser-js": "^1.0.36", + "universal-cookie": "^4.0.4" }, "devDependencies": { "@titicaca/triple-web": "workspace:*", diff --git a/packages/triple-web-binding-nextjs-pages/src/session.ts b/packages/triple-web-binding-nextjs-pages/src/session.ts new file mode 100644 index 0000000000..d65e01a9b0 --- /dev/null +++ b/packages/triple-web-binding-nextjs-pages/src/session.ts @@ -0,0 +1,146 @@ +import { NextPageContext } from 'next' +import { + GET_USER_REQUEST_URL, + SessionProviderValue, + SessionUser, + checkClientApp, +} from '@titicaca/triple-web' +import { + ssrFetcherize, + captureHttpError, + authFetcherize, + post, + get, +} from '@titicaca/fetcher' +import Cookies from 'universal-cookie' + +/** + * - app (server-side): refresh X + * - app (client-side): refresh X + * - browser (server-side) refresh O + * - browser (client-side) refresh O + * @returns + */ +export async function getSession( + ctx: NextPageContext, +): Promise { + const userAgent = ctx.req + ? ctx.req.headers['user-agent'] ?? '' + : window.navigator.userAgent + + const isClientApp = checkClientApp(userAgent) + + const user = await fetchUser(ctx, isClientApp) + + return { + initialSession: { + user, + }, + } +} + +async function fetchUser(ctx: NextPageContext, isClientApp: boolean) { + if (ctx.req) { + // Server-side + + // 세션이 없으면 fetch를 스킵합니다. + const cookies = new Cookies(ctx.req.headers.cookie) + + let hasSession = false + + if (isClientApp) { + hasSession = !!cookies.get('x-soto-session') + } else { + hasSession = !!ctx.req.headers['x-triple-web-login'] + if (process.env.NODE_ENV !== 'production') { + hasSession = !!cookies.get('TP_SE') + } + } + + if (!hasSession) { + return null + } + + // fetch 시작 + const ssrFetcherizeOptions = { + apiUriBase: process.env.API_URI_BASE || '', + cookie: ctx.req.headers.cookie, + } + + if (isClientApp) { + const finalFetcher = ssrFetcherize(get, ssrFetcherizeOptions) + + const response = await finalFetcher(GET_USER_REQUEST_URL) + + if (response.status !== 401) { + captureHttpError(response) + } + + if (response.ok === false) { + return null + } + + return response.parsedBody + } else { + const finalFetcher = authFetcherize( + ssrFetcherize(get, ssrFetcherizeOptions), + { + refresh: () => + ssrFetcherize( + post, + ssrFetcherizeOptions, + )('/api/users/web-session/token'), + }, + ) + + const response = await finalFetcher(GET_USER_REQUEST_URL) + + if (response === 'NEED_LOGIN') { + return null + } + + captureHttpError(response) + + if (response.ok === false) { + return null + } + + return response.parsedBody + } + } else { + // Client-side + if (isClientApp) { + const finalFetcher = get + + const response = await finalFetcher(GET_USER_REQUEST_URL) + + if (response.status !== 401) { + captureHttpError(response) + } + + if (response.ok === false) { + return null + } + + return response.parsedBody + } else { + const finalFetcher = authFetcherize(get, { + refresh: () => post('/api/users/web-session/token'), + }) + + const response = await finalFetcher(GET_USER_REQUEST_URL) + + if (response === 'NEED_LOGIN') { + return null + } + + captureHttpError(response) + + if (response.ok === false) { + return null + } + + return response.parsedBody + } + } +} diff --git a/packages/triple-web-binding-nextjs/package.json b/packages/triple-web-binding-nextjs/package.json index 022000ecec..3a5fccdbbe 100644 --- a/packages/triple-web-binding-nextjs/package.json +++ b/packages/triple-web-binding-nextjs/package.json @@ -39,6 +39,8 @@ ] }, "dependencies": { + "@titicaca/fetcher": "^13.8.1", + "@titicaca/view-utilities": "^13.8.1", "server-only": "^0.0.1", "ua-parser-js": "^1.0.36" }, diff --git a/packages/triple-web-binding-nextjs/src/session.ts b/packages/triple-web-binding-nextjs/src/session.ts new file mode 100644 index 0000000000..6add92974b --- /dev/null +++ b/packages/triple-web-binding-nextjs/src/session.ts @@ -0,0 +1,109 @@ +import 'server-only' + +import { cookies, headers } from 'next/headers' +import { + ssrFetcherize, + captureHttpError, + authFetcherize, + post, + get, +} from '@titicaca/fetcher' +import { + GET_USER_REQUEST_URL, + checkClientApp, + SessionUser, + Session, +} from '@titicaca/triple-web' + +/** + * - app: refresh X + * - browser: refresh O + * @returns + */ +export async function getSession(): Promise { + const headersList = headers() + const userAgent = headersList.get('user-agent') ?? '' + + const isClientApp = checkClientApp(userAgent) + const hasSession = checkSession(isClientApp) + + if (!hasSession) { + return { + user: null, + } + } + + const user = await fetchUser(isClientApp) + + return { + user, + } +} + +function checkSession(isClientApp: boolean) { + const headersList = headers() + const cookiesList = cookies() + + let hasSession = false + + if (isClientApp) { + hasSession = cookiesList.has('x-soto-session') + } else { + hasSession = headersList.has('x-triple-web-login') + + if (process.env.NODE_ENV !== 'production') { + hasSession = cookiesList.has('TP_SE') + } + } + + return hasSession +} + +async function fetchUser(isClientApp: boolean) { + const headersList = headers() + + const ssrFetcherizeOptions = { + apiUriBase: process.env.API_URI_BASE || '', + cookie: headersList.get('cookie') ?? undefined, + } + + if (isClientApp) { + const finalFetcher = ssrFetcherize(get, ssrFetcherizeOptions) + + const response = await finalFetcher(GET_USER_REQUEST_URL) + + if (response.status !== 401) { + captureHttpError(response) + } + + if (response.ok === false) { + return null + } + + return response.parsedBody + } else { + const finalFetcher = authFetcherize( + ssrFetcherize(get, ssrFetcherizeOptions), + { + refresh: () => + ssrFetcherize( + post, + ssrFetcherizeOptions, + )('/api/users/web-session/token'), + }, + ) + const response = await finalFetcher(GET_USER_REQUEST_URL) + + if (response === 'NEED_LOGIN') { + return null + } + + captureHttpError(response) + + if (response.ok === false) { + return null + } + + return response.parsedBody + } +} diff --git a/packages/triple-web/package.json b/packages/triple-web/package.json index 6654dcff50..33cd3ca762 100644 --- a/packages/triple-web/package.json +++ b/packages/triple-web/package.json @@ -39,7 +39,11 @@ ] }, "dependencies": { + "@titicaca/fetcher": "^13.9.0", + "@titicaca/view-utilities": "^13.9.0", + "@types/qs": "^6.9.8", "@types/semver": "^7.5.3", + "qs": "^6.11.2", "semver": "^7.5.4", "ua-parser-js": "^1.0.36" }, diff --git a/packages/triple-web/src/contexts/index.ts b/packages/triple-web/src/contexts/index.ts index 8ec7c45a06..434ce54f77 100644 --- a/packages/triple-web/src/contexts/index.ts +++ b/packages/triple-web/src/contexts/index.ts @@ -1,3 +1,4 @@ export * from './client-app' export * from './env' +export * from './session' export * from './user-agent' diff --git a/packages/triple-web/src/contexts/session.tsx b/packages/triple-web/src/contexts/session.tsx new file mode 100644 index 0000000000..2a62adcaa5 --- /dev/null +++ b/packages/triple-web/src/contexts/session.tsx @@ -0,0 +1,71 @@ +import { + Dispatch, + PropsWithChildren, + SetStateAction, + createContext, + useState, +} from 'react' + +export interface SessionUser { + name: string + provider: Provider + country: string + lang: string + unregister: boolean | null + photo: string + mileage: Mileage + uid: string +} + +type Provider = 'TRIPLE' | 'NAVER' | 'KAKAO' | 'FACEBOOK' | 'APPLE' + +interface Mileage { + badges: { + icon: { + imageUrl: string + } + }[] + level: number + point: number +} + +export interface Session { + user: SessionUser | null +} + +export const SessionStateContext = createContext(undefined) +export const SessionUpdaterContext = createContext< + Dispatch> | undefined +>(undefined) + +export interface SessionProviderValue { + initialSession: Session +} + +export interface SessionProviderProps extends PropsWithChildren { + value: SessionProviderValue | undefined +} + +export function SessionProvider({ children, value }: SessionProviderProps) { + if (value === undefined) { + return <>{children} + } + + return {children} +} + +interface InnerSessionProviderProps extends PropsWithChildren { + value: SessionProviderValue +} + +function InnerSessionProvider({ children, value }: InnerSessionProviderProps) { + const [session, setSession] = useState(value.initialSession) + + return ( + + + {children} + + + ) +} diff --git a/packages/triple-web/src/hooks/client-app/index.ts b/packages/triple-web/src/hooks/client-app/index.ts new file mode 100644 index 0000000000..7e743bc545 --- /dev/null +++ b/packages/triple-web/src/hooks/client-app/index.ts @@ -0,0 +1,2 @@ +export * from './use-client-app' +export * from './use-feature-flag' diff --git a/packages/triple-web/src/hooks/use-client-app.ts b/packages/triple-web/src/hooks/client-app/use-client-app.ts similarity index 79% rename from packages/triple-web/src/hooks/use-client-app.ts rename to packages/triple-web/src/hooks/client-app/use-client-app.ts index cce5a1f25c..c27a1778db 100644 --- a/packages/triple-web/src/hooks/use-client-app.ts +++ b/packages/triple-web/src/hooks/client-app/use-client-app.ts @@ -1,6 +1,6 @@ import { useContext } from 'react' -import { ClientAppContext } from '../contexts/client-app' +import { ClientAppContext } from '../../contexts/client-app' export function useClientApp() { const context = useContext(ClientAppContext) diff --git a/packages/triple-web/src/hooks/use-feature-flag.ts b/packages/triple-web/src/hooks/client-app/use-feature-flag.ts similarity index 100% rename from packages/triple-web/src/hooks/use-feature-flag.ts rename to packages/triple-web/src/hooks/client-app/use-feature-flag.ts diff --git a/packages/triple-web/src/hooks/env/index.ts b/packages/triple-web/src/hooks/env/index.ts new file mode 100644 index 0000000000..1bac4181c7 --- /dev/null +++ b/packages/triple-web/src/hooks/env/index.ts @@ -0,0 +1 @@ +export * from './use-env' diff --git a/packages/triple-web/src/hooks/use-env.ts b/packages/triple-web/src/hooks/env/use-env.ts similarity index 81% rename from packages/triple-web/src/hooks/use-env.ts rename to packages/triple-web/src/hooks/env/use-env.ts index 5230f7ace9..248a0606a3 100644 --- a/packages/triple-web/src/hooks/use-env.ts +++ b/packages/triple-web/src/hooks/env/use-env.ts @@ -1,6 +1,6 @@ import { useContext } from 'react' -import { EnvContext } from '../contexts' +import { EnvContext } from '../../contexts' export function useEnv() { const context = useContext(EnvContext) diff --git a/packages/triple-web/src/hooks/index.ts b/packages/triple-web/src/hooks/index.ts index 0ce73aec8d..434ce54f77 100644 --- a/packages/triple-web/src/hooks/index.ts +++ b/packages/triple-web/src/hooks/index.ts @@ -1,3 +1,4 @@ -export * from './use-client-app' -export * from './use-env' -export * from './use-user-agent' +export * from './client-app' +export * from './env' +export * from './session' +export * from './user-agent' diff --git a/packages/triple-web/src/hooks/session/index.ts b/packages/triple-web/src/hooks/session/index.ts new file mode 100644 index 0000000000..cf7a3ac94d --- /dev/null +++ b/packages/triple-web/src/hooks/session/index.ts @@ -0,0 +1,3 @@ +export * from './use-login' +export * from './use-logout' +export * from './use-session' diff --git a/packages/triple-web/src/hooks/session/use-login.ts b/packages/triple-web/src/hooks/session/use-login.ts new file mode 100644 index 0000000000..b6902c4cf3 --- /dev/null +++ b/packages/triple-web/src/hooks/session/use-login.ts @@ -0,0 +1,44 @@ +import { useCallback, useContext } from 'react' +import qs from 'qs' +import { generateUrl } from '@titicaca/view-utilities' + +import { ClientAppContext } from '../../contexts' +import { useEnv } from '../env' + +export interface LoginOptions { + returnUrl?: string +} + +export function useLogin() { + const clientApp = useContext(ClientAppContext) + const env = useEnv() + + return useCallback( + (options: LoginOptions) => { + if (clientApp) { + handleClientApp(env.appUrlScheme) + } else { + handleBrowser(options.returnUrl) + } + }, + [clientApp, env.appUrlScheme], + ) +} + +function handleClientApp(appUrlScheme: string) { + const loginUrl = generateUrl({ scheme: appUrlScheme, path: '/login' }) + + window.location.href = loginUrl +} + +function handleBrowser(returnUrl: string | undefined) { + const loginUrl = generateUrl({ + path: '/login', + query: qs.stringify({ + returnUrl: + returnUrl ?? window.location.href.replace(window.location.origin, ''), + }), + }) + + window.location.href = loginUrl +} diff --git a/packages/triple-web/src/hooks/session/use-logout.ts b/packages/triple-web/src/hooks/session/use-logout.ts new file mode 100644 index 0000000000..526a8ad8fc --- /dev/null +++ b/packages/triple-web/src/hooks/session/use-logout.ts @@ -0,0 +1,33 @@ +import { useCallback, useContext } from 'react' +import { authGuardedFetchers } from '@titicaca/fetcher' + +import { ClientAppContext, SessionUpdaterContext } from '../../contexts' + +export function useLogout() { + const clientApp = useContext(ClientAppContext) + const setSession = useContext(SessionUpdaterContext) + + if (setSession === undefined) { + throw new Error() + } + + return useCallback(async () => { + setSession({ user: null }) + + if (clientApp) { + await handleClientApp() + } else { + await handleBrowser() + } + }, [clientApp, setSession]) +} + +function handleClientApp() { + return Promise.resolve() +} + +async function handleBrowser() { + await authGuardedFetchers.put('/api/users/logout') + + window.location.reload() +} diff --git a/packages/triple-web/src/hooks/session/use-session.ts b/packages/triple-web/src/hooks/session/use-session.ts new file mode 100644 index 0000000000..5236f06af9 --- /dev/null +++ b/packages/triple-web/src/hooks/session/use-session.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react' + +import { SessionStateContext } from '../../contexts' + +export function useSession() { + const context = useContext(SessionStateContext) + + if (context === undefined) { + throw new Error('') + } + + return context +} diff --git a/packages/triple-web/src/hooks/user-agent/index.ts b/packages/triple-web/src/hooks/user-agent/index.ts new file mode 100644 index 0000000000..8c6e350cdc --- /dev/null +++ b/packages/triple-web/src/hooks/user-agent/index.ts @@ -0,0 +1 @@ +export * from './use-user-agent' diff --git a/packages/triple-web/src/hooks/use-user-agent.ts b/packages/triple-web/src/hooks/user-agent/use-user-agent.ts similarity index 78% rename from packages/triple-web/src/hooks/use-user-agent.ts rename to packages/triple-web/src/hooks/user-agent/use-user-agent.ts index ac3112acb7..37c992415e 100644 --- a/packages/triple-web/src/hooks/use-user-agent.ts +++ b/packages/triple-web/src/hooks/user-agent/use-user-agent.ts @@ -1,6 +1,6 @@ import { useContext } from 'react' -import { UserAgentContext } from '../contexts/user-agent' +import { UserAgentContext } from '../../contexts/user-agent' export function useUserAgent() { const context = useContext(UserAgentContext) diff --git a/packages/triple-web/src/triple-web.tsx b/packages/triple-web/src/triple-web.tsx index de84fb469f..ee2ff96747 100644 --- a/packages/triple-web/src/triple-web.tsx +++ b/packages/triple-web/src/triple-web.tsx @@ -2,12 +2,20 @@ import { PropsWithChildren } from 'react' -import { Env, EnvContext, UserAgent, UserAgentContext } from './contexts' +import { + Env, + EnvContext, + SessionProvider, + SessionProviderValue, + UserAgent, + UserAgentContext, +} from './contexts' import { ClientApp, ClientAppContext } from './contexts/client-app' export interface TripleWebProps extends PropsWithChildren { clientAppProvider?: ClientApp envProvider?: Env + sessionProvider?: SessionProviderValue userAgentProvider?: UserAgent } @@ -15,14 +23,17 @@ export function TripleWeb({ children, clientAppProvider, envProvider, + sessionProvider, userAgentProvider, }: TripleWebProps) { return ( - - {children} - + + + {children} + + ) diff --git a/packages/triple-web/src/utils/check-client-app.ts b/packages/triple-web/src/utils/check-client-app.ts new file mode 100644 index 0000000000..99da943c17 --- /dev/null +++ b/packages/triple-web/src/utils/check-client-app.ts @@ -0,0 +1,5 @@ +import { clientAppRegex } from './regex' + +export function checkClientApp(userAgent: string) { + return clientAppRegex.test(userAgent) +} diff --git a/packages/triple-web/src/utils/index.ts b/packages/triple-web/src/utils/index.ts index fb62355e64..3462542f5b 100644 --- a/packages/triple-web/src/utils/index.ts +++ b/packages/triple-web/src/utils/index.ts @@ -1 +1,4 @@ +export * from './check-client-app' export * from './parse-client-app-metadata' +export * from './regex' +export * from './user' diff --git a/packages/triple-web/src/utils/parse-client-app-metadata.ts b/packages/triple-web/src/utils/parse-client-app-metadata.ts index ebff2a1f58..e39413a530 100644 --- a/packages/triple-web/src/utils/parse-client-app-metadata.ts +++ b/packages/triple-web/src/utils/parse-client-app-metadata.ts @@ -1,9 +1,11 @@ import { ClientApp, ClientAppName } from '../contexts' +import { clientAppRegex } from './regex' + export function parseClientAppMetadata( userAgent: string, ): NonNullable['metadata'] | null { - const matchData = userAgent.match(/Triple-(iOS|Android)\/([^ ]+)/i) + const matchData = clientAppRegex.exec(userAgent) if (!matchData) { return null diff --git a/packages/triple-web/src/utils/regex.ts b/packages/triple-web/src/utils/regex.ts new file mode 100644 index 0000000000..18e7c10316 --- /dev/null +++ b/packages/triple-web/src/utils/regex.ts @@ -0,0 +1 @@ +export const clientAppRegex = /Triple-(iOS|Android)\/([^ ]+)/g diff --git a/packages/triple-web/src/utils/user.ts b/packages/triple-web/src/utils/user.ts new file mode 100644 index 0000000000..85eb471de7 --- /dev/null +++ b/packages/triple-web/src/utils/user.ts @@ -0,0 +1 @@ +export const GET_USER_REQUEST_URL = '/api/users/me' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc2917c2f3..63db94aeb9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -890,9 +890,21 @@ importers: packages/triple-web: dependencies: + '@titicaca/fetcher': + specifier: ^13.9.0 + version: link:../fetcher + '@titicaca/view-utilities': + specifier: ^13.9.0 + version: link:../view-utilities + '@types/qs': + specifier: ^6.9.8 + version: 6.9.9 '@types/semver': specifier: ^7.5.3 version: 7.5.3 + qs: + specifier: ^6.11.2 + version: 6.11.2 semver: specifier: ^7.5.4 version: 7.5.4 @@ -909,6 +921,12 @@ importers: packages/triple-web-binding-nextjs: dependencies: + '@titicaca/fetcher': + specifier: ^13.8.1 + version: link:../fetcher + '@titicaca/view-utilities': + specifier: ^13.8.1 + version: link:../view-utilities server-only: specifier: ^0.0.1 version: 0.0.1 @@ -928,9 +946,18 @@ importers: packages/triple-web-binding-nextjs-pages: dependencies: + '@titicaca/fetcher': + specifier: ^13.8.1 + version: link:../fetcher + '@titicaca/view-utilities': + specifier: ^13.8.1 + version: link:../view-utilities ua-parser-js: specifier: ^1.0.36 version: 1.0.36 + universal-cookie: + specifier: ^4.0.4 + version: 4.0.4 devDependencies: '@titicaca/triple-web': specifier: workspace:* @@ -8210,7 +8237,6 @@ packages: /@types/qs@6.9.9: resolution: {integrity: sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==} - dev: true /@types/range-parser@1.2.4: resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==}