diff --git a/apps/web/src/app/(afterLogin)/layout.tsx b/apps/web/src/app/(afterLogin)/layout.tsx index ed7de26..7a50ac1 100644 --- a/apps/web/src/app/(afterLogin)/layout.tsx +++ b/apps/web/src/app/(afterLogin)/layout.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren } from 'react' -import { userService, UserStatus } from '@vook-client/api' -import { cookies, headers } from 'next/headers' +import { UserInfoResponse, userService, UserStatus } from '@vook-client/api' +import { cookies } from 'next/headers' import { redirect } from 'next/navigation' import { dehydrate, HydrationBoundary } from '@tanstack/react-query' @@ -15,24 +15,30 @@ import { mainArea } from './layout.css' const Layout = async ({ children }: PropsWithChildren) => { const cookieStore = cookies() - const isAuthorization = headers().get('X-AuthConfirm') - - if (isAuthorization !== 'confirmed') { - redirect('/login') - } const access = cookieStore.get('access')?.value || '' const refresh = cookieStore.get('refresh')?.value || '' - if (!access && !refresh) { + if (!access || !refresh) { redirect('/login') } const queryClient = getQueryClient() + queryClient.setQueryData(['access'], access) queryClient.setQueryData(['refresh'], refresh) - const user = await userService.userInfo(queryClient) + let user: UserInfoResponse + + try { + user = await userService.userInfo(queryClient) + } catch { + redirect('/login') + } + + if (user.result.onboardingCompleted === false) { + redirect('/onboarding') + } if (user.result.status !== UserStatus.Registered) { redirect('/signup') diff --git a/apps/web/src/app/(afterLogin)/vocabulary/[id]/_component/term/Term.css.ts b/apps/web/src/app/(afterLogin)/vocabulary/[id]/_component/term/Term.css.ts index 4a3b445..012897d 100644 --- a/apps/web/src/app/(afterLogin)/vocabulary/[id]/_component/term/Term.css.ts +++ b/apps/web/src/app/(afterLogin)/vocabulary/[id]/_component/term/Term.css.ts @@ -36,7 +36,7 @@ export const termTitleContainer = style({ }) export const highlightHit = style({ - backgroundColor: vars.colors['component-alternative'], + backgroundColor: vars.colors['semantic-line-alternative'], }) export const highlight = style({ diff --git a/apps/web/src/app/(afterLogin)/workspace/layout.tsx b/apps/web/src/app/(afterLogin)/workspace/layout.tsx index fb5e247..2929f55 100644 --- a/apps/web/src/app/(afterLogin)/workspace/layout.tsx +++ b/apps/web/src/app/(afterLogin)/workspace/layout.tsx @@ -1,36 +1,20 @@ import { PropsWithChildren } from 'react' -import { userService, UserStatus } from '@vook-client/api' -import { cookies, headers } from 'next/headers' -import { redirect } from 'next/navigation' +import { cookies } from 'next/headers' import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import { getQueryClient } from '@/utils/react-query' -const Layout = async ({ children }: PropsWithChildren) => { +const Layout = ({ children }: PropsWithChildren) => { const cookieStore = cookies() - const isAuthorization = headers().get('X-AuthConfirm') - - if (isAuthorization !== 'confirmed') { - redirect('/login') - } const access = cookieStore.get('access')?.value || '' const refresh = cookieStore.get('refresh')?.value || '' - if (!access && !refresh) { - redirect('/login') - } - const queryClient = getQueryClient() + queryClient.setQueryData(['access'], access) queryClient.setQueryData(['refresh'], refresh) - const user = await userService.userInfo(queryClient) - - if (user.result.status !== UserStatus.Registered) { - redirect('/signup') - } - const dehydrateState = dehydrate(queryClient) return ( diff --git a/apps/web/src/app/(beforeLogin)/auth/page.tsx b/apps/web/src/app/(beforeLogin)/auth/page.tsx new file mode 100644 index 0000000..8459836 --- /dev/null +++ b/apps/web/src/app/(beforeLogin)/auth/page.tsx @@ -0,0 +1,7 @@ +import { redirect } from 'next/navigation' + +const Page = () => { + return redirect('/login') +} + +export default Page diff --git a/apps/web/src/app/(beforeLogin)/auth/token/page.tsx b/apps/web/src/app/(beforeLogin)/auth/token/page.tsx index 1cdd8f0..5bb31b5 100644 --- a/apps/web/src/app/(beforeLogin)/auth/token/page.tsx +++ b/apps/web/src/app/(beforeLogin)/auth/token/page.tsx @@ -32,14 +32,6 @@ const AuthCallbackPage = ({ secure: true, expires: new Date('2038-01-19T03:14:07.000Z'), }) - window.postMessage( - { - from: 'vook-web', - access, - refresh, - }, - '*', - ) queryClient.setQueryData(['access'], access) queryClient.setQueryData(['refresh'], refresh) diff --git a/apps/web/src/app/(onboarding)/onboarding/job/page.tsx b/apps/web/src/app/(onboarding)/onboarding/job/page.tsx index 0e2c4b7..b5f9566 100644 --- a/apps/web/src/app/(onboarding)/onboarding/job/page.tsx +++ b/apps/web/src/app/(onboarding)/onboarding/job/page.tsx @@ -6,12 +6,10 @@ import { SelectBox, Text, } from '@vook-client/design-system' -import React from 'react' +import React, { useEffect } from 'react' import { OnboardingJob, useOnboardingMutation } from '@vook-client/api' import { useRouter } from 'next/navigation' -import { Link } from '@/components/Link' - import { SelectBoxGroup } from '../_components/SelectBoxGroup' import { useOnBoarding } from '../_context/useOnboarding' import { OnboardingHeader } from '../_components/OnboardingHeader' @@ -76,10 +74,17 @@ const OnboardingJobPage = () => { }, ) - const onSubmitFunnel = () => { - if (!mutation.isPending || !mutation.isSuccess) { - mutation.mutate() + useEffect(() => { + window.onpopstate = () => { + if (location.pathname === '/onboarding/job') { + alert('뒤로가기를 통한 접근을 감지하여 페이지를 이동합니다.') + router.push('/workspace') + } } + }, [router]) + + const onSubmitFunnel = () => { + mutation.mutate() } const onClickJob = (job: OnboardingJob) => { @@ -122,22 +127,20 @@ const OnboardingJobPage = () => { 건너뛰기 - - - + ) diff --git a/apps/web/src/app/(onboarding)/onboarding/layout.tsx b/apps/web/src/app/(onboarding)/onboarding/layout.tsx index d5c3e33..c83faa8 100644 --- a/apps/web/src/app/(onboarding)/onboarding/layout.tsx +++ b/apps/web/src/app/(onboarding)/onboarding/layout.tsx @@ -10,21 +10,29 @@ import { OnBoardingProvider } from './_context/useOnboarding' const Layout = async ({ children }: PropsWithChildren) => { const cookieStore = cookies() + const accessToken = cookieStore.get('access')?.value const refreshToken = cookieStore.get('refresh')?.value - if (!accessToken && !refreshToken) { + if (!accessToken || !refreshToken) { redirect('/login') } const queryClient = getQueryClient() + queryClient.setQueryData(['access'], accessToken) queryClient.setQueryData(['refresh'], refreshToken) - const userInfo = await userService.userInfo(queryClient) + let userInfo + + try { + userInfo = await userService.userInfo(queryClient) + } catch { + redirect('/login') + } if (userInfo.result.onboardingCompleted) { - redirect('/') + redirect('/workspace') } return ( diff --git a/apps/web/src/components/InitialSetting/InitialSetting.tsx b/apps/web/src/components/InitialSetting/InitialSetting.tsx index 5292b75..19fc9f8 100644 --- a/apps/web/src/components/InitialSetting/InitialSetting.tsx +++ b/apps/web/src/components/InitialSetting/InitialSetting.tsx @@ -2,14 +2,20 @@ import { baseFetcher } from '@vook-client/api' import { useLayoutEffect } from 'react' +import Cookies from 'js-cookie' +import { useQueryClient } from '@tanstack/react-query' import { useToast } from '@/hooks/useToast' export const InitialSetting = () => { const { addToast } = useToast() + const client = useQueryClient() useLayoutEffect(() => { // eslint-disable-next-line promise/prefer-await-to-callbacks + client.setQueryData(['access'], Cookies.get('access')) + client.setQueryData(['refresh'], Cookies.get('refresh')) + baseFetcher.setUnAuthorizedHandler(() => { location.href = '/login' }) @@ -19,6 +25,14 @@ export const InitialSetting = () => { type: 'error', }) }) + baseFetcher.setTokenRefreshHandler((access, refresh) => { + Cookies.set('access', access, { + expires: new Date('2038-01-19T03:14:07.000Z'), + }) + Cookies.set('refresh', refresh, { + expires: new Date('2038-01-19T03:14:07.000Z'), + }) + }) }, [addToast]) return null diff --git a/apps/web/src/components/ProfileEditForm/ProfileEditForm.tsx b/apps/web/src/components/ProfileEditForm/ProfileEditForm.tsx index b559b6a..41f76d9 100644 --- a/apps/web/src/components/ProfileEditForm/ProfileEditForm.tsx +++ b/apps/web/src/components/ProfileEditForm/ProfileEditForm.tsx @@ -27,7 +27,7 @@ export const ProfileEditForm = () => { const userEditMutation = useEditUserMutation( { - nickname, + nickname: nickname.trim(), }, { onSuccess: () => { @@ -56,7 +56,7 @@ export const ProfileEditForm = () => { useEffect( function checkValidateNickname() { - const isBlankNickname = nickname.length === 0 + const isBlankNickname = nickname.trim().length === 0 const isSameNickname = nickname === user?.nickname if (isBlankNickname || isSameNickname) { diff --git a/apps/web/src/components/SignUpForm/SignUpForm.tsx b/apps/web/src/components/SignUpForm/SignUpForm.tsx index 41f960c..5e57d1c 100644 --- a/apps/web/src/components/SignUpForm/SignUpForm.tsx +++ b/apps/web/src/components/SignUpForm/SignUpForm.tsx @@ -80,13 +80,11 @@ export const SignUpForm = () => { [signUpMutation.isPending], ) - const canSubmit = useMemo( - () => - !formState.isValid || - signUpMutation.isPending || - signUpMutation.isSuccess, - [formState.isValid, signUpMutation.isPending, signUpMutation.isSuccess], - ) + const canSubmit = + watch('nickname').trim().length === 0 || + !formState.isValid || + signUpMutation.isPending || + signUpMutation.isSuccess if (!userInfoQuery.data) { return null diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts deleted file mode 100644 index 340966c..0000000 --- a/apps/web/src/middleware.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { - ACCESS_TOKEN_HEADER_KEY, - REFRESH_TOKEN_HEADER_KEY, - UserInfoResponse, - UserStatus, -} from 'node_modules/@vook-client/api' - -/** - * 권한 검사를 위한 미들웨어 생성 함수 - * - * - 토큰이 모두 없는 경우: destination으로 리다이렉트 - * - access 토큰이 없는 경우: refresh 토큰을 이용해 새로운 access 토큰을 발급 후 권한 확인 및 토큰 갱신 - * - access 토큰이 있지만 유효하지 않은 경우: refresh 토큰을 이용해 새로운 access 토큰을 발급 후 권한 확인 및 토큰 갱신 - * - refresh 토큰이 만료된 경우: destination으로 리다이렉트 - * - 유저 상태가 허용되지 않는 경우: destination으로 리다이렉트 - * - * @param roles 접근 권한이 허용된 유저 상태 - */ -const checkUserStatusMiddleware = - (roles: Array) => - async ( - req: NextRequest, - finalResponse: NextResponse, - destination: string, - ) => { - const accessToken = req.cookies.get('access')?.value - const refreshToken = req.cookies.get('refresh')?.value - const loginRedirectResponse = NextResponse.redirect( - `${process.env.NEXT_PUBLIC_DOMAIN}${destination}`, - ) - - let newAccessToken: string | null = null - let newRefreshToken: string | null = null - - const tokenGenerate = async (refresh: string) => { - const res = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`, - { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - [REFRESH_TOKEN_HEADER_KEY]: refresh, - }, - }, - ) - if (res.ok) { - newAccessToken = res.headers.get(ACCESS_TOKEN_HEADER_KEY) - newRefreshToken = res.headers.get(REFRESH_TOKEN_HEADER_KEY) - - finalResponse.cookies.set('access', newAccessToken!, { - expires: new Date('2038-01-19T03:14:07.000Z'), - maxAge: 60 * 60 * 24 * 365 * 20, - }) - finalResponse.cookies.set('refresh', newRefreshToken!, { - expires: new Date('2038-01-19T03:14:07.000Z'), - maxAge: 60 * 60 * 24 * 365 * 20, - }) - } else { - return false - } - - return true - } - - const fetchUserInfo = async (token: string) => { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/user/info`, { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - [ACCESS_TOKEN_HEADER_KEY]: token, - }, - }) - if (res.ok) { - return res.json() as Promise - } - return null - } - - const isBothTokenMissing = !accessToken && !refreshToken - - if (isBothTokenMissing) { - return loginRedirectResponse - } - - const isAccessTokenMissing = !accessToken && refreshToken - - if (isAccessTokenMissing) { - const success = await tokenGenerate(refreshToken) - - if (!success) { - return loginRedirectResponse - } - } - - let userInfo = await fetchUserInfo(newAccessToken || accessToken || '') - - if (!userInfo) { - const success = await tokenGenerate(refreshToken!) - - if (!success) { - return loginRedirectResponse - } - - userInfo = await fetchUserInfo(newAccessToken || accessToken || '') - - if (!userInfo) { - return loginRedirectResponse - } - } - - if (!roles.includes(userInfo.result.status)) { - return loginRedirectResponse - } - - finalResponse.headers.set('X-AuthConfirm', 'confirmed') - - return finalResponse - } - -const onlyRegisteredMatch = [ - '/onboarding', - '/user/edit', - '/workspace', - '/vocabulary/', -] - -const onlyRegisteredMiddleware = checkUserStatusMiddleware([ - UserStatus.Registered, - UserStatus.SocialLoginCompleted, -]) - -const onlyUnregisteredSocialUser = checkUserStatusMiddleware([ - UserStatus.SocialLoginCompleted, - UserStatus.Withdrawn, -]) - -export async function middleware(req: NextRequest) { - const requestHeaders = new Headers(req.headers) - requestHeaders.set('X-pathname', req.nextUrl.pathname) - - const response = NextResponse.next({ - request: { - headers: requestHeaders, - }, - }) - - if ( - onlyRegisteredMatch.some((url) => req.nextUrl.pathname.includes(url)) || - req.nextUrl.pathname.includes('/vocabulary/') - ) { - return onlyRegisteredMiddleware(req, response, '/login') - } - - if (req.nextUrl.pathname === '/signup') { - return onlyUnregisteredSocialUser(req, response, '/login') - } - - return response -} diff --git a/packages/api/src/lib/fetcher.ts b/packages/api/src/lib/fetcher.ts index a58e651..42c8e7c 100644 --- a/packages/api/src/lib/fetcher.ts +++ b/packages/api/src/lib/fetcher.ts @@ -115,8 +115,9 @@ export class Fetcher { if (response.ok) { data = (await response.json()) as Promise } else { - const error = (await response.json()) as { code: string } - throw new Error(error.code) + const error = (await response.json()) as { result: string } + + throw new Error(error.result) } } catch (error) { // eslint-disable-next-line no-console diff --git a/packages/api/src/services/user/queries.ts b/packages/api/src/services/user/queries.ts index e63f4c1..4c8ad3b 100644 --- a/packages/api/src/services/user/queries.ts +++ b/packages/api/src/services/user/queries.ts @@ -20,6 +20,7 @@ import { export const userOptions = { userInfo: (client: QueryClient) => ({ queryKey: [], + staleTime: 0, queryFn: () => userService.userInfo(client), }), onboarding: (client: QueryClient, dto: OnboardingDTO) => ({ diff --git a/packages/design-system/src/tokens/colors.ts b/packages/design-system/src/tokens/colors.ts index f6d2136..6e992b7 100644 --- a/packages/design-system/src/tokens/colors.ts +++ b/packages/design-system/src/tokens/colors.ts @@ -12,7 +12,7 @@ const semantic = { 'semantic-label-disabled': 'rgba(22, 23, 25, 0.16)', /* line */ 'semantic-line-normal': 'rgba(112, 115, 124, 0.22)', - 'semantic-line-alternative': 'rgba(112, 115, 124, 0.08)', + 'semantic-line-alternative': 'rgba(112, 115, 124, 0.05)', } const link = {