generated from ttipo/project-base
feat(frontend): 13단계 전환 준비 화면 — 새 비밀번호 · 2단계 켜기 단계 · 유예 줄 · 메일 프로그램 연결 · 30일 안내 · 문의 줄
- 제한된 세션: restriction 이 있으면 보호 화면 대신 로그인 틀의 그 단계(새 비밀번호 → 2단계 켜기)로, AUTH_010 은 알림 없이 me 를 다시 받아 옮긴다. 제한된 동안은 나가기 확인을 묻지 않고(확인에서 이동을 취소하면 메일 화면에 갇혔다 — 리뷰) 실시간 알림을 열지 않는다. - 로그인 때 넣은 비밀번호는 반응형이 아닌 클로저에 10분 · 세션이 끝나면 지운다. 2단계를 켠 직원의 새 비밀번호 단계는 다음 코드를 받는다. - 유예 노란 줄(시작일 전날까지), 설정 › 2단계 인증의 필수 안내, 설정 › 메일 프로그램 연결(펼침 줄 · 새 Outlook 은 권하지 않음), 휴지통 · 스팸메일함 30일 줄, 로그인 · 코드 단계의 문의 줄. 검증: lint · type-check · 단위 2,354 · 실사용 시험 89/89. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
|
||||
import { fail, installMockApi, restoreApiAdapter } from '@/test-utils/mock-api'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
import { fail, installMockApi, ok, restoreApiAdapter } from '@/test-utils/mock-api'
|
||||
import router from './index'
|
||||
|
||||
// 라우터 가드 — 공개 화면(11단계 내려받기 /s/:token)은 세션을 묻지 않는다 (계약 4절 "화면 /s/{token}")
|
||||
// 받는 쪽은 우리 직원이 아니다 — 쿠키 없는 바깥 브라우저에서 /auth/me · /auth/refresh 가 오가지 않게
|
||||
|
||||
const TOKEN = 'Qx7mK2vR9pL4wT8nB3cF6hJ1dS5gA0eY2uI7oP9kZ3x'
|
||||
const USER = { email: 'ara.kim@arasystem.test', name: '김아라' }
|
||||
|
||||
describe('router 가드', () => {
|
||||
beforeEach(() => {
|
||||
@@ -41,4 +43,26 @@ describe('router 가드', () => {
|
||||
expect(router.currentRoute.value.name).toBe('login')
|
||||
expect(router.currentRoute.value.query.redirect).toBe('/settings/mail/large-files')
|
||||
})
|
||||
|
||||
// 제한된 세션 (13단계 — 계약 6절) — 로그인은 됐지만 새 비밀번호 · 2단계 켜기를 마칠 때까지 보호 화면 대신 로그인 틀의 그 단계
|
||||
it('제한된 세션이면 보호 화면 대신 로그인으로 — 돌아올 곳은 가려던 주소', async () => {
|
||||
installMockApi(() => ok({ ...USER, restriction: 'passwordChange', twoFactorDeadline: null }))
|
||||
|
||||
await router.push('/contacts')
|
||||
|
||||
expect(useAuthStore().isLoggedIn).toBe(true)
|
||||
expect(router.currentRoute.value.name).toBe('login')
|
||||
expect(router.currentRoute.value.query.redirect).toBe('/contacts')
|
||||
})
|
||||
|
||||
it('제한된 세션은 로그인 화면에 머문다 (그 단계를 그린다) — 제한이 풀리면 가려던 주소로 보낸다', async () => {
|
||||
installMockApi(() => ok({ ...USER, restriction: 'twoFactorSetup', twoFactorDeadline: null }))
|
||||
|
||||
await router.push('/login?redirect=/mail/inbox')
|
||||
expect(router.currentRoute.value.name).toBe('login')
|
||||
|
||||
useAuthStore().markAuthenticated(USER, { restriction: null, twoFactorDeadline: null })
|
||||
await router.push('/login?redirect=/settings/account/two-factor')
|
||||
expect(router.currentRoute.value.fullPath).toBe('/settings/account/two-factor')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -35,11 +35,12 @@ router.beforeEach(async (to) => {
|
||||
await useAuth().restoreSession()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
|
||||
// 로그인 후 원래 가려던 화면으로 돌아온다
|
||||
// 로그인 전 · 제한된 세션(13단계 — 새 비밀번호 · 2단계 켜기를 마치기 전, 계약 6절)은 보호 화면 대신 로그인 틀로.
|
||||
// 로그인 화면이 제한의 단계를 그리고, 끝나면 원래 가려던 화면으로 돌아온다
|
||||
if (to.meta.requiresAuth && (!authStore.isLoggedIn || authStore.restriction !== null)) {
|
||||
return { name: 'login', query: { redirect: to.fullPath } }
|
||||
}
|
||||
if (to.meta.guestOnly && authStore.isLoggedIn) {
|
||||
if (to.meta.guestOnly && authStore.isLoggedIn && authStore.restriction === null) {
|
||||
return toSafeRedirect(to.query.redirect) ?? { name: 'mail' }
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -96,7 +96,7 @@ export const routes: RouteRecordRaw[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
// 설정 — 왼쪽 메뉴(계정 › 비밀번호 · 2단계 인증 · 앱 비밀번호 · 로그인 기록 / 메일 › 서명 · 부재중 자동 응답 · 필터 · 보내기 · 읽기 ·
|
||||
// 설정 — 왼쪽 메뉴(계정 › 비밀번호 · 2단계 인증 · 앱 비밀번호 · 메일 프로그램 연결 · 로그인 기록 / 메일 › 서명 · 부재중 자동 응답 · 필터 · 보내기 · 읽기 ·
|
||||
// 차단한 주소 · 대용량 첨부 · 알림) + 하위 화면. /settings 는 첫 메뉴(계정 › 비밀번호)로 바꿔 끼운다 (앱 레일의 "설정")
|
||||
path: '/settings',
|
||||
component: () => import('@/pages/settings/SettingsLayout.vue'),
|
||||
@@ -122,6 +122,13 @@ export const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/pages/settings/AccountAppPasswordsPage.vue'),
|
||||
meta: { title: '앱 비밀번호' },
|
||||
},
|
||||
{
|
||||
// 메일 프로그램 연결 (13단계 — 디자인 README "13단계 › 설정 › 계정 › 메일 프로그램 연결") — 화면 상수로만 그린다
|
||||
path: 'account/mail-clients',
|
||||
name: SETTINGS_ROUTES.mailClients,
|
||||
component: () => import('@/pages/settings/AccountMailClientsPage.vue'),
|
||||
meta: { title: '메일 프로그램 연결' },
|
||||
},
|
||||
{
|
||||
path: 'account/login-history',
|
||||
name: SETTINGS_ROUTES.loginHistory,
|
||||
|
||||
Reference in New Issue
Block a user