Skip to content

Commit

Permalink
Merge pull request #66 from Myongji-Graduate/auth/#62
Browse files Browse the repository at this point in the history
Auth/#62
  • Loading branch information
seonghunYang authored Apr 5, 2024
2 parents 0c08f83 + dacf65f commit da0c9fc
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 34 deletions.
1 change: 1 addition & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const config: StorybookConfig = {
...config.resolve.alias,
'@': path.resolve(__dirname, '../'),
'next/headers': path.resolve(__dirname, '../app/utils/test/__mock__/next/headers.ts'),
'next/navigation': path.resolve(__dirname, '../app/utils/test/__mock__/next/navigation.ts'),
};
}
return config;
Expand Down
43 changes: 41 additions & 2 deletions app/business/user/user.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,49 @@

import { FormState } from '@/app/ui/view/molecule/form/form-root';
import { API_PATH } from '../api-path';
import { SignUpRequestBody, SignInRequestBody } from './user.type';
import { SignUpRequestBody, SignInRequestBody, ValidateTokenResponse } from './user.type';
import { httpErrorHandler } from '@/app/utils/http/http-error-handler';
import { BadRequestError } from '@/app/utils/http/http-error';
import { SignUpFormSchema, SignInFormSchema, SignInResponseSchema } from './user.validation';
import {
SignUpFormSchema,
SignInFormSchema,
SignInResponseSchema,
ValidateTokenResponseSchema,
} from './user.validation';
import { cookies } from 'next/headers';
import { isValidation } from '@/app/utils/zod/validation.util';
import { redirect } from 'next/navigation';

export async function validateToken(): Promise<ValidateTokenResponse | boolean> {
const accessToken = cookies().get('accessToken')?.value;
const refreshToken = cookies().get('refreshToken')?.value;
try {
const response = await fetch(`${API_PATH.auth}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ refreshToken }),
});

const result = await response.json();

httpErrorHandler(response, result);

if (isValidation(result, ValidateTokenResponseSchema)) {
return result;
} else {
throw 'Invalid token response schema.';
}
} catch (error) {
if (error instanceof BadRequestError) {
return false;
} else {
throw error;
}
}
}

export async function authenticate(prevState: FormState, formData: FormData): Promise<FormState> {
const validatedFields = SignInFormSchema.safeParse({
Expand Down Expand Up @@ -52,6 +89,8 @@ export async function authenticate(prevState: FormState, formData: FormData): Pr
secure: process.env.NODE_ENV === 'production',
path: '/',
});

redirect('/my');
}
} catch (error) {
if (error instanceof BadRequestError) {
Expand Down
28 changes: 28 additions & 0 deletions app/business/user/user.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { httpErrorHandler } from '@/app/utils/http/http-error-handler';
import { API_PATH } from '../api-path';
import { UserInfoResponse } from './user.type';
import { cookies } from 'next/headers';
import { isValidation } from '@/app/utils/zod/validation.util';
import { UserInfoResponseSchema } from './user.validation';

export async function getUserInfo(): Promise<UserInfoResponse> {
try {
const response = await fetch(`${API_PATH.user}`, {
headers: {
Authorization: `Bearer ${cookies().get('accessToken')?.value}`,
},
});

const result = await response.json();

httpErrorHandler(response, result);

if (isValidation(result, UserInfoResponseSchema)) {
return result;
} else {
throw 'Invalid user info response schema.';
}
} catch (error) {
throw error;
}
}
6 changes: 5 additions & 1 deletion app/business/user/user.type.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// https://stackoverflow.com/questions/76957592/error-only-async-functions-are-allowed-to-be-exported-in-a-use-server-file
// server action 파일에서는 async function만 export 가능

import { SignInResponseSchema } from './user.validation';
import { SignInResponseSchema, UserInfoResponseSchema, ValidateTokenResponseSchema } from './user.validation';
import z from 'zod';

export interface SignUpRequestBody {
Expand All @@ -17,3 +17,7 @@ export interface SignInRequestBody {
}

export type SignInResponse = z.infer<typeof SignInResponseSchema>;

export type UserInfoResponse = z.infer<typeof UserInfoResponseSchema>;

export type ValidateTokenResponse = z.infer<typeof ValidateTokenResponseSchema>;
12 changes: 12 additions & 0 deletions app/business/user/user.validation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { z } from 'zod';

// api 변경 예정
export const UserInfoResponseSchema = z.object({
studentNumber: z.string(),
studentName: z.string(),
major: z.string(),
isSumbitted: z.boolean(),
});

export const ValidateTokenResponseSchema = z.object({
accessToken: z.string(),
});

export const SignInFormSchema = z.object({
authId: z.string(),
password: z.string(),
Expand Down
39 changes: 35 additions & 4 deletions app/mocks/db.mock.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { TakenLectures } from '../business/lecture/taken-lecture.query';
import { SignUpRequestBody, SignInRequestBody } from '../business/user/user.type';
import { SignUpRequestBody, SignInRequestBody, UserInfoResponse } from '../business/user/user.type';
import { takenLectures } from './data.mock';

interface MockUser {
authId: string;
password: string;
studentNumber: string;
engLv: string;
major?: string;
major: string;
isSumbitted: boolean;
name: string;
}

interface MockDatabaseState {
Expand All @@ -18,8 +20,9 @@ interface MockDatabaseState {
type MockDatabaseAction = {
getTakenLectures: () => TakenLectures[];
getUser: (authId: string) => MockUser | undefined;
createUser: (user: MockUser) => boolean;
createUser: (user: SignUpRequestBody) => boolean;
signIn: (userData: SignInRequestBody) => boolean;
getUserInfo: (authId: string) => UserInfoResponse;
};

export const mockDatabase: MockDatabaseAction = {
Expand All @@ -29,13 +32,38 @@ export const mockDatabase: MockDatabaseAction = {
if (mockDatabaseStore.users.find((u) => u.authId === user.authId || u.studentNumber === user.studentNumber)) {
return false;
}
mockDatabaseStore.users = [...mockDatabaseStore.users, user];
mockDatabaseStore.users = [
...mockDatabaseStore.users,
{
...user,
isSumbitted: false,
major: '융소입니다',
name: '모킹이2',
},
];
return true;
},
signIn: (userData: SignInRequestBody) => {
const user = mockDatabaseStore.users.find((u) => u.authId === userData.authId && u.password === userData.password);
return !!user;
},
getUserInfo: (authId: string) => {
const user = mockDatabaseStore.users.find((u) => u.authId === authId);
if (!user) {
return {
studentNumber: '',
studentName: '',
major: '',
isSumbitted: false,
};
}
return {
studentNumber: user.studentNumber,
studentName: user.name,
major: user.major,
isSumbitted: user.isSumbitted,
};
},
};

const initialState: MockDatabaseState = {
Expand All @@ -46,6 +74,9 @@ const initialState: MockDatabaseState = {
password: 'admin',
studentNumber: '60000000',
engLv: 'ENG12',
isSumbitted: false,
major: '융소입니다',
name: '모킹이',
},
],
};
Expand Down
40 changes: 38 additions & 2 deletions app/mocks/handlers/user-handler.mock.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,47 @@
import { HttpResponse, http, delay } from 'msw';
import { API_PATH } from '../../business/api-path';
import { mockDatabase } from '../db.mock';
import { SignUpRequestBody, SignInRequestBody, SignInResponse } from '@/app/business/user/user.type';
import {
SignUpRequestBody,
SignInRequestBody,
SignInResponse,
ValidateTokenResponse,
UserInfoResponse,
} from '@/app/business/user/user.type';
import { ErrorResponseData } from '@/app/utils/http/http-error-handler';

function mockDecryptToken(token: string) {
if (token === 'fake-access-token') {
return {
authId: 'admin',
};
}
return {
authId: '',
};
}

export const userHandlers = [
http.post<never, never, ValidateTokenResponse>(`${API_PATH.auth}/token`, async ({ request }) => {
return HttpResponse.json({
accessToken: 'fake-access-token',
});
}),
http.get<never, never, UserInfoResponse | ErrorResponseData>(`${API_PATH.user}`, async ({ request }) => {
const accessToken = request.headers.get('Authorization')?.replace('Bearer ', '');
if (!accessToken) {
return HttpResponse.json({ status: 401, message: 'Unauthorized' }, { status: 401 });
}

const userInfo = mockDatabase.getUserInfo(mockDecryptToken(accessToken).authId);
await delay(500);

if (!userInfo) {
return HttpResponse.json({ status: 401, message: 'Unauthorized' }, { status: 401 });
}

return HttpResponse.json(userInfo);
}),
http.post<never, SignUpRequestBody, never>(`${API_PATH.user}/sign-up`, async ({ request }) => {
const userData = await request.json();

Expand All @@ -17,7 +54,6 @@ export const userHandlers = [

return HttpResponse.json({ status: 200 });
}),

http.post<never, SignInRequestBody, SignInResponse | ErrorResponseData>(
`${API_PATH.auth}/sign-in`,
async ({ request }) => {
Expand Down
22 changes: 3 additions & 19 deletions app/ui/user/sign-in-form/sign-in-form.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,15 @@ const meta = {
);
},
],
args: {
onNext: fn(),
},
} as Meta<typeof SignInForm>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};

const beforeEach = () => {
resetMockDB();
mockDatabase.createUser({
authId: 'testtest',
password: 'test1234!',
studentNumber: '60000001',
engLv: 'ENG12',
});
};

export const SuccessSenario: Story = {
play: async ({ args, canvasElement, step }) => {
beforeEach();
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);

await step('사용자가 양식에 맞춰서 폼을 입력하면', async () => {
Expand All @@ -60,14 +46,13 @@ export const SuccessSenario: Story = {
});

await step('로그인에 성공한다', async () => {
await waitFor(() => expect(args.onNext).toHaveBeenCalled());
await waitFor(() => expect(canvas.queryByText('아이디 또는 비밀번호가 일치하지 않습니다.')).toBeNull());
});
},
};

export const FailureScenarioWithoutUser: Story = {
play: async ({ args, canvasElement, step }) => {
beforeEach();
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);

await step('사용자가 존재하지 않는 아이디로 로그인하면', async () => {
Expand All @@ -78,7 +63,6 @@ export const FailureScenarioWithoutUser: Story = {
});

await step('로그인에 실패한다', async () => {
await waitFor(() => expect(args.onNext).not.toHaveBeenCalled());
expect(await canvas.findByText('아이디 또는 비밀번호가 일치하지 않습니다.')).toBeInTheDocument();
});
},
Expand Down
8 changes: 2 additions & 6 deletions app/ui/user/sign-in-form/sign-in-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@
import Form from '../../view/molecule/form';
import { authenticate } from '@/app/business/user/user.command';

interface SignInFormProps {
onNext?: () => void;
}

export default function SignInForm({ onNext }: SignInFormProps) {
export default function SignInForm() {
return (
<Form onSuccess={onNext} id="로그인" action={authenticate}>
<Form id="로그인" action={authenticate}>
<Form.TextInput required={true} label="아이디" id="authId" placeholder="아이디를 입력하세요" />
<Form.PasswordInput required={true} label="비밀번호" id="password" placeholder="비밀번호를 입력하세요" />
<Form.SubmitButton label="로그인" position="center" variant="primary" />
Expand Down
1 change: 1 addition & 0 deletions app/utils/test/__mock__/next/navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export function redirect() {}
Loading

0 comments on commit da0c9fc

Please sign in to comment.