Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth 시스템과 연결합니다. #30

Merged
merged 15 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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): 동아리원들을 위한 그룹웨어 시스템이에요.
- [playground](/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.3.0",
"@tanstack/react-query": "^5.17.19",
"classnames": "^2.5.1",
"next": "14.1.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/member/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"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"
Expand Down
20 changes: 20 additions & 0 deletions apps/member/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import { useEffect, useState } from 'react';
import { useToken } from '@hooks/common/useToken';
import AppRouter from '@router/AppRouter';
import { useSetIsLoggedInStore } from '@store/auth';

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

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

if (isLoading) {
return null;
}

return <AppRouter />;
};

Expand Down
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: 4 additions & 5 deletions apps/member/src/components/common/Nav/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@ 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 className="font-bold">
PLAY<span className="font-medium">GROUND</span>
</h1>
</Link>
<div className="hidden gap-10 text-sm font-semibold sm:flex">
Expand Down
26 changes: 18 additions & 8 deletions apps/member/src/components/my/ProfileSection/ProfileSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Button } from '@clab/design-system';
import Image from '@components/common/Image/Image';
import Section from '@components/common/Section/Section';
import { useSetIsLoggedInStore } from '@store/auth';
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from '@constants/api';

interface ProfileSectionProps {
data: {
Expand All @@ -21,6 +23,14 @@ interface ProfileInfoProps {
}

const ProfileSection = ({ data }: ProfileSectionProps) => {
const setIsLoggedIn = useSetIsLoggedInStore();

const onClickLogout = () => {
sessionStorage.removeItem(ACCESS_TOKEN_KEY);
sessionStorage.removeItem(REFRESH_TOKEN_KEY);
setIsLoggedIn(false);
};

const { image, name, id, interests, phone, email, githubUrl, address } = data;

return (
Expand All @@ -30,7 +40,7 @@ const ProfileSection = ({ data }: ProfileSectionProps) => {
<Button color="orange" size="sm">
수정
</Button>
<Button color="red" size="sm">
<Button color="red" size="sm" onClick={onClickLogout}>
로그아웃
</Button>
</div>
Expand All @@ -41,7 +51,7 @@ const ProfileSection = ({ data }: ProfileSectionProps) => {
width="w-32"
height="h-32"
src={image}
alt="프로필 이미지"
alt={name}
className="rounded-full m-auto object-cover"
/>
<div className="mt-2">
Expand All @@ -50,18 +60,18 @@ const ProfileSection = ({ data }: ProfileSectionProps) => {
</div>
</div>
<div className="mt-4 space-y-4">
<ProfileInfo label="분야">{interests}</ProfileInfo>
<ProfileInfo label="연락처">{phone}</ProfileInfo>
<ProfileInfo label="이메일">{email}</ProfileInfo>
<ProfileInfo label="주소">{address}</ProfileInfo>
<ProfileInfo label="Github">{githubUrl}</ProfileInfo>
<ProfileInfoRow label="분야">{interests}</ProfileInfoRow>
<ProfileInfoRow label="연락처">{phone}</ProfileInfoRow>
<ProfileInfoRow label="이메일">{email}</ProfileInfoRow>
<ProfileInfoRow label="주소">{address}</ProfileInfoRow>
<ProfileInfoRow label="Github">{githubUrl}</ProfileInfoRow>
</div>
</Section.Body>
</Section>
);
};

const ProfileInfo = ({ label, children }: ProfileInfoProps) => {
const ProfileInfoRow = ({ label, children }: ProfileInfoProps) => {
return (
<div className="flex">
<p className="w-24 font-semibold">{label}</p>
Expand Down
32 changes: 32 additions & 0 deletions apps/member/src/components/router/ProtectAuth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { PATH } from '@constants/path';
import { useGetIsLoggedInStore } from '@store/auth';
import { Navigate, Outlet } from 'react-router-dom';

interface ProtectAuthProps {
protect?: boolean;
children?: React.ReactNode;
}

/**
* protect가 true일 경우 로그인 상태일 경우만 접근 가능한 페이지
* false일 경우 로그인 상태일 경우 접근 불가능한 페이지
*/
const ProtectAuth = ({ protect = false, children }: ProtectAuthProps) => {
const isLoggedIn = useGetIsLoggedInStore();

if (protect && !isLoggedIn) {
// 로그인이 필요한 페이지에 로그인이 되어있지 않은 경우
return <Navigate to={PATH.LOGIN} />;
} else if (!protect && isLoggedIn) {
// 로그인이 되어있는데, 로그인이 없어야 하는 페이지에 접근한 경우
return <Navigate to={PATH.MAIN} />;
}

if (children) {
return children;
}

return <Outlet />;
};

export default ProtectAuth;
3 changes: 3 additions & 0 deletions apps/member/src/constants/key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const ATOM_KEY = {
IS_LOGGED_IN: 'isLoggedInState',
};
2 changes: 2 additions & 0 deletions apps/member/src/constants/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export const PATH = {
LIBRARY_DETAIL: '/library/:id',
SUPPORT: '/support',
SITEMAP: '/sitemap',
LOGIN: '/login',
AUTH: '/auth',
};

export const PATH_FINDER = {
Expand Down
Empty file.
13 changes: 13 additions & 0 deletions apps/member/src/hooks/common/useToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from '@constants/api';

export const useToken = () => {
const accessToken = sessionStorage.getItem(ACCESS_TOKEN_KEY);
const refreshToken = sessionStorage.getItem(REFRESH_TOKEN_KEY);

if (!accessToken || !refreshToken) {
sessionStorage.removeItem(ACCESS_TOKEN_KEY);
sessionStorage.removeItem(REFRESH_TOKEN_KEY);
}

return [accessToken, refreshToken] as const;
};
37 changes: 37 additions & 0 deletions apps/member/src/pages/AuthPage/AuthPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect } from 'react';
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from '@constants/api';
import { PATH } from '@constants/path';
import { useSetIsLoggedInStore } from '@store/auth';
import { useSearchParams, useNavigate } from 'react-router-dom';

const AuthPage = () => {
const [searchParams] = useSearchParams();
const setIsLoggedIn = useSetIsLoggedInStore();
const navigate = useNavigate();

useEffect(() => {
// URL 쿼리 파라미터에서 토큰을 가져옵니다.
const accessToken = searchParams.get('a');
const refreshToken = searchParams.get('r');

if (accessToken && refreshToken) {
// 토큰이 있으면 토큰을 저장하고 메인 페이지로 이동합니다.
sessionStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
sessionStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
setIsLoggedIn(true);
navigate(PATH.MAIN);
return;
} else {
// 토큰이 없으면 로그인 페이지로 이동합니다.
sessionStorage.removeItem(ACCESS_TOKEN_KEY);
sessionStorage.removeItem(REFRESH_TOKEN_KEY);
setIsLoggedIn(false);
navigate(PATH.LOGIN);
return;
}
}, [navigate, searchParams, setIsLoggedIn]);

return <h1>로그인 중...</h1>;
};

export default AuthPage;
Loading