Skip to content

Commit

Permalink
Merge pull request #33 from KGU-C-Lab/feat/auth
Browse files Browse the repository at this point in the history
  • Loading branch information
gwansikk authored Feb 5, 2024
2 parents 9fa965f + 8e94f95 commit 37d6d29
Show file tree
Hide file tree
Showing 60 changed files with 877 additions and 274 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
tsconfig.tsbuildinfo

.env

.yarn/*
!.yarn/releases
!.yarn/plugins
Expand All @@ -12,5 +14,4 @@ tsconfig.tsbuildinfo
node_modules

.DS_Store

.idea
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

- [auth](/apps/auth/README.md): 동아리 계정을 이용한 로그인(OAuth) 시스템이에요.
- [land](/apps/land/README.md): 외부인이 동아리에 대해 알아볼 수 있는 랜딩 페이지이에요.
- [member](/apps/member/README.md): 동아리원들을 위한 그룹웨어 시스템이에요.
- [play](/apps/member/README.md): 동아리 활동을 위한 그룹웨어 시스템이에요.

## Packages

Expand Down
16 changes: 14 additions & 2 deletions apps/auth/app/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ServerResponse } from '@type/server';
import { END_POINTS } from '../constants/api';
import { server } from './server';

Expand All @@ -7,14 +8,25 @@ interface PostLoginBody {
password: string;
}

interface PostLoginResponse extends ServerResponse {
data: string | null;
}

interface PostTwoFactorLoginResponse extends ServerResponse {
data: {
accessToken: string;
refreshToken: string;
};
}

interface PostTwoFactorLoginBody {
[key: string]: string;
memberId: string;
totp: string;
}

export const postLogin = async (body: PostLoginBody) => {
const response = await server.post({
const response = await server.post<PostLoginBody, PostLoginResponse>({
url: END_POINTS.LOGIN,
body,
});
Expand All @@ -26,7 +38,7 @@ export const postLogin = async (body: PostLoginBody) => {
};

export const postTwoFactorLogin = async (body: PostTwoFactorLoginBody) => {
return await server.post({
return await server.post<PostTwoFactorLoginBody, PostTwoFactorLoginResponse>({
url: END_POINTS.TWO_FACTOR_LOGIN,
body,
});
Expand Down
4 changes: 2 additions & 2 deletions apps/auth/app/constants/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export const ERROR_MESSAGE = {
export const REDIRECT = (code: string) => {
return (
{
'clab.page': 'https://clab.page/login',
dev: 'http://localhost:6002/login',
'clab.page': 'https://member.clab.page/auth',
dev: 'http://localhost:6002/auth',
}[code] || 'https://clab.page'
);
};
6 changes: 2 additions & 4 deletions apps/auth/app/hooks/queries/useTwoFactorLoginMutation.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { useMutation } from '@tanstack/react-query';
import { AUTH_ATOM_DEFAULT, useSetAuthStore } from '@store/auth';
import { postTwoFactorLogin } from '@api/auth';
import { useSearchParams } from 'next/navigation';
import { ERROR_MESSAGE, REDIRECT, SUCCESS_MESSAGE } from '@constants/api';
import { useCode } from '@hooks/useCode';

export const useTwoFactorLoginMutation = () => {
const setAuth = useSetAuthStore();
const searchParams = useSearchParams();

const code = searchParams.get('code');
const { code } = useCode();

const twoFactorLoginMutation = useMutation({
mutationFn: postTwoFactorLogin,
Expand Down
4 changes: 4 additions & 0 deletions apps/auth/app/types/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ServerResponse<T = unknown> {
success: boolean;
data: T;
}
6 changes: 5 additions & 1 deletion apps/auth/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
experimental: {
missingSuspenseWithCSRBailout: false,
},
};

export default nextConfig;
2 changes: 1 addition & 1 deletion apps/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"@gwansikk/server-chain": "^0.2.0",
"@gwansikk/server-chain": "^0.4.4",
"@tanstack/react-query": "^5.17.19",
"classnames": "^2.5.1",
"next": "14.1.0",
Expand Down
2 changes: 2 additions & 0 deletions apps/member/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VITE_MODE=환경모드
VITE_API_BASE_URL=API주소
5 changes: 3 additions & 2 deletions apps/member/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --port 6002",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@gwansikk/server-chain": "^0.4.4",
"@tanstack/react-query": "^5.18.1",
"classnames": "^2.5.1",
"dayjs": "^1.11.10",
"framer-motion": "^10.18.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.0.1",
"react-query": "^3.39.3",
"react-router-dom": "^6.21.2",
"recoil": "^0.7.7"
},
Expand Down
23 changes: 23 additions & 0 deletions apps/member/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
import { useEffect, useState } from 'react';
import { useToken } from '@hooks/common/useToken';
import AppRouter from '@router/AppRouter';
import { useSetIsLoggedInStore } from '@store/auth';
import { server } from '@api/server';
import { authorization } from '@utils/api';

const App = () => {
const setIsLoggedIn = useSetIsLoggedInStore();
const [accessToken, refreshToken] = useToken();
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
if (accessToken && refreshToken) {
setIsLoggedIn(true);
server.setHeaders(authorization(accessToken));
} else {
setIsLoggedIn(false);
}
setIsLoading(false);
}, [accessToken, refreshToken, setIsLoggedIn]);

if (isLoading) {
return null;
}

return <AppRouter />;
};

Expand Down
Empty file removed apps/member/src/api/.gitkeep
Empty file.
14 changes: 14 additions & 0 deletions apps/member/src/api/board.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { PaginationType } from '@type/api';
import { server } from './server';
import { END_POINT } from '@constants/api';
import type { BoardItem } from '@type/board';
import { createPagination } from '@utils/api';

// 내가 작성한 커뮤니티 게시글 조회
export const getMyBoards = async (page: number, size: number) => {
const { data } = await server.get<PaginationType<BoardItem>>({
url: createPagination(END_POINT.MY_BOARDS, page, size),
});

return data;
};
16 changes: 16 additions & 0 deletions apps/member/src/api/book.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { PaginationType } from '@type/api';
import { server } from './server';
import { createPagination } from '@utils/api';
import { END_POINT } from '@constants/api';
import { BookItem } from '@type/book';

// 나의 대출내역 조회
export const getMyBooks = async (page: number, size: number, id: number) => {
const { data } = await server.get<PaginationType<BookItem>>({
url: createPagination(END_POINT.MY_BOOKS, page, size),
});
const myLoanBooks = data.items.filter(
(book) => book.borrowerId === String(id),
);
return myLoanBooks;
};
14 changes: 14 additions & 0 deletions apps/member/src/api/comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { PaginationType } from '@type/api';
import { server } from './server';
import { createPagination } from '@utils/api';
import { END_POINT } from '@constants/api';
import type { CommentItem } from '@type/comment';

// 나의 댓글 조회
export const getMyComments = async (page: number, size: number) => {
const { data } = await server.get<PaginationType<CommentItem>>({
url: createPagination(END_POINT.MY_COMMENTS, page, size),
});

return data;
};
75 changes: 75 additions & 0 deletions apps/member/src/api/interceptors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { server } from './server';
import { API_BASE_URL, END_POINT } from '@constants/api';
import type { Interceptor } from '@gwansikk/server-chain';
import type { BaseResponse, TokenType } from '@type/api';
import {
authorization,
getAccessToken,
getRefreshToken,
removeTokens,
setTokens,
} from '@utils/api';

let reissueLock = false;

const retryRequest = async (
response: Response,
method: string,
delay = 300,
) => {
await new Promise((resolve) => setTimeout(resolve, delay)); // 재요청 딜레이

const accessToken = getAccessToken();
return fetch(response.url, {
method: method,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
};

export const tokenHandler: Interceptor<Response> = async (response, method) => {
const { status } = response;

if (status === 401) {
const preRefreshToken = getRefreshToken();
if (!preRefreshToken) return response;

if (reissueLock) {
// 잠금이 걸려있는 경우, 토큰 갱신 될 때까지 재요청
return retryRequest(response, method);
} else {
// 토큰 갱신 중복 요청 방지
reissueLock = true;
}

const tokenResponse = await fetch(
`${API_BASE_URL}${END_POINT.LOGIN_REISSUE}`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${preRefreshToken}`,
'Content-Type': 'application/json',
},
},
);

const { success, data } =
(await tokenResponse.json()) as BaseResponse<TokenType>;

if (success === true) {
const { accessToken, refreshToken } = data;
setTokens(accessToken, refreshToken);
server.setHeaders(authorization(accessToken));
reissueLock = false;
return retryRequest(response, method, 0);
} else {
// 토큰 갱신에 실패한 경우 로그아웃 처리
removeTokens();
window.location.reload();
}
}

return response;
};
19 changes: 19 additions & 0 deletions apps/member/src/api/member.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { server } from './server';
import { END_POINT } from '@constants/api';
import type { BaseResponse } from '@type/api';
import type { MemberType } from '@type/member';

interface PatchUserInfoArgs {
id: string;
body: MemberType;
}

// 내 정보 수정
export const patchUserInfo = async ({ id, body }: PatchUserInfoArgs) => {
const { data } = await server.patch<MemberType, BaseResponse<MemberType>>({
url: END_POINT.MY_INFO_EDIT(id),
body: body,
});

return data;
};
14 changes: 14 additions & 0 deletions apps/member/src/api/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { PaginationType } from '@type/api';
import { server } from './server';
import type { NotificationItem } from '@type/notification';
import { createPagination } from '@utils/api';
import { END_POINT } from '@constants/api';

// 나의 알림 조회
export const getMyNotifications = async (page: number, size: number) => {
const { data } = await server.get<PaginationType<NotificationItem>>({
url: createPagination(END_POINT.MY_NOTIFICATION, page, size),
});

return data;
};
14 changes: 14 additions & 0 deletions apps/member/src/api/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import ServerChain from '@gwansikk/server-chain';
import { tokenHandler } from './interceptors';
import { API_BASE_URL } from '@constants/api';

export const server = ServerChain({
key: 'server',
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
interceptors: {
error: tokenHandler,
},
});
21 changes: 21 additions & 0 deletions apps/member/src/components/common/LoginButton/LoginButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Image from '../Image/Image';

const LoginButton = () => {
const handleClick = () => {
window.location.href = 'https://auth.clab.page/?code=dev';
};

return (
<button
onClick={handleClick}
className="max-w-xs flex items-center border border-black py-2 px-4 bg-[#292d32] gap-4 rounded-md w-full"
>
<Image src="/logo.webp" alt="C-Lab" width="w-8" height="h-8" />
<span className="text-white font-semibold text-center grow">
C-Lab Auth로 로그인
</span>
</button>
);
};

export default LoginButton;
9 changes: 3 additions & 6 deletions apps/member/src/components/common/Nav/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,14 @@ const Nav = () => {
const onClickReady = () => {
alert('준비중');
};

return (
<nav className="fixed left-0 top-0 z-50 w-full border-b bg-white">
<div className="section flex h-[61px] items-center justify-between py-1.5">
<div className="flex items-center gap-10 lg:gap-20">
<Link
className="flex items-center gap-2 text-xl font-bold"
to={PATH.MAIN}>
<Link className="flex items-center gap-2 text-xl" to={PATH.MAIN}>
<img src="/favicon.ico" alt="c-lab" className="size-8" />
<h1>
씨랩<span className="font-medium">오피스</span>
</h1>
<h1 className="font-bold">PLAY</h1>
</Link>
<div className="hidden gap-10 text-sm font-semibold sm:flex">
<Link to={PATH.MAIN}></Link>
Expand Down
Loading

0 comments on commit 37d6d29

Please sign in to comment.