Skip to content

Commit

Permalink
Merge pull request #98 from sopt-makers/feature-admin_authority
Browse files Browse the repository at this point in the history
[#96][FEATURE] 계정권한 분리, 디버그 툴 구현
  • Loading branch information
Brokyeom authored Dec 1, 2023
2 parents 580b9bf + 74924a0 commit 38dfe91
Show file tree
Hide file tree
Showing 14 changed files with 188 additions and 21 deletions.
7 changes: 7 additions & 0 deletions src/components/common/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useRouter } from 'next/router';

import AdminStatusDevtools from '@/components/devTools/AdminStatus';
import { destroyToken } from '@/utils/auth';

import { StHeader } from './style';
Expand All @@ -14,6 +15,12 @@ function Header() {

return (
<StHeader>
{process.env.NEXT_PUBLIC_API_URL !== 'PRODUCTION' && (
<div className="status_devtools">
<AdminStatusDevtools />
</div>
)}

<button onClick={logout}>
<p>로그아웃</p>
</button>
Expand Down
5 changes: 5 additions & 0 deletions src/components/common/Header/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export const StHeader = styled.header`
height: 8rem;
padding: 0 2.6rem;
div.status_devtools {
width: fit-content;
padding: 2rem;
}
button {
width: 10.2rem;
padding: 0.3rem 0.6rem;
Expand Down
11 changes: 9 additions & 2 deletions src/components/common/Nav/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { useRouter } from 'next/router';
import { Fragment } from 'react';
import { Fragment, useContext } from 'react';

import { adminStatusContext } from '@/components/devTools/AdminContextProvider';
import { MENU_LIST } from '@/utils/nav';

import GenerationDropDown from './GenerationDropDown';
import { StMenu, StNavWrapper, StSoptLogo, StSubMenu } from './style';

function Nav() {
const router = useRouter();
const { status } = useContext(adminStatusContext);

const filteredMenuList =
status === 'MAKERS'
? MENU_LIST.filter((menu) => menu.title === '알림 관리')
: MENU_LIST;

const handleSubMenuClick = (path: string) => {
router.push(path);
Expand All @@ -19,7 +26,7 @@ function Nav() {
<StSoptLogo>SOPT ADMIN</StSoptLogo>
</header>
<GenerationDropDown />
{MENU_LIST.map((menu) => (
{filteredMenuList.map((menu) => (
<Fragment key={menu.title}>
<StMenu
currentPage={
Expand Down
44 changes: 44 additions & 0 deletions src/components/devTools/AdminContextProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useEffect,
useState,
} from 'react';

interface Props {
children: ReactNode;
}

interface AdminStatusContextType {
status: string | null;
setStatus: Dispatch<SetStateAction<string | null>>;
}

const defaultContextValue: AdminStatusContextType = {
status: 'DEVELOPER',
setStatus: () => {},
};

export const adminStatusContext =
createContext<AdminStatusContextType>(defaultContextValue);

export const AdminStatusProvider = (props: Props) => {
const { children } = props;
const [status, setStatus] = useState<string | null>('NOT_CERTIFIED');

useEffect(() => {
if (typeof window !== 'undefined') {
const savedStatus =
sessionStorage.getItem('adminStatus') ?? 'NOT_CERTIFIED';
setStatus(savedStatus);
}
}, []);

return (
<adminStatusContext.Provider value={{ status, setStatus }}>
{children}
</adminStatusContext.Provider>
);
};
59 changes: 59 additions & 0 deletions src/components/devTools/AdminStatus/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useRouter } from 'next/router';
import { useContext, useEffect, useState } from 'react';

import DropDown from '@/components/common/DropDown';
import OptionTemplate from '@/components/common/OptionTemplate';
import Selector from '@/components/common/Selector';

import { adminStatusContext } from '../AdminContextProvider';
import { StAdminStatusContainer } from './style';

function AdminStatusDevtools() {
const router = useRouter();
const [isDropdownOpen, setIsDropdownOpen] = useState<boolean>(false);
const { status, setStatus } = useContext(adminStatusContext);

const STATUS_SELECTION: ADMIN_STATUS[] = ['SOPT', 'MAKERS'];

const isAdminStatus = (value: string): value is ADMIN_STATUS => {
return [
'SUPER_USER',
'SOPT',
'MAKERS',
'NOT_CERTIFIED',
'DEVELOPER',
].includes(value);
};

const handleStatusChange = (newStatus: string) => {
if (isAdminStatus(newStatus)) {
setStatus(newStatus);
sessionStorage.setItem('adminStatus', newStatus);
if (newStatus === 'MAKERS') {
router.push('/alarmAdmin');
}
} else {
console.error('유효하지 않은 권한입니다.');
}
};

return (
<StAdminStatusContainer>
<OptionTemplate title="권한">
<Selector
content={status}
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
/>
{isDropdownOpen && (
<DropDown
type={'select'}
list={STATUS_SELECTION}
onItemSelected={handleStatusChange}
/>
)}
</OptionTemplate>
</StAdminStatusContainer>
);
}

export default AdminStatusDevtools;
5 changes: 5 additions & 0 deletions src/components/devTools/AdminStatus/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import styled from '@emotion/styled';

export const StAdminStatusContainer = styled.div`
width: 18.5rem;
`;
16 changes: 16 additions & 0 deletions src/hooks/useUnauthorizedStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useRouter } from 'next/router';
import { useEffect } from 'react';

export const useUnauthorizedStatus = (status: ADMIN_STATUS) => {
const router = useRouter();

useEffect(() => {
if (
typeof window !== 'undefined' &&
sessionStorage.getItem('adminStatus') === status
) {
alert('접근 권한이 없는 계정입니다.');
router.back();
}
}, [router, status]);
};
25 changes: 14 additions & 11 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ReactQueryDevtools } from 'react-query/devtools';
import { RecoilRoot } from 'recoil';

import Layout from '@/components/common/Layout';
import { AdminStatusProvider } from '@/components/devTools/AdminContextProvider';
import global from '@/styles/global';
import theme from '@/styles/theme';
import { getToken } from '@/utils/auth';
Expand Down Expand Up @@ -35,17 +36,19 @@ export default function App({ Component, pageProps }: AppProps) {
<title>SOPT Admin</title>
</Head>
<QueryClientProvider client={client}>
<Hydrate state={pageProps.dehydratedState}>
<RecoilRoot>
<ThemeProvider theme={theme}>
<Global styles={global} />
<Layout>
<Component {...pageProps} />
</Layout>
</ThemeProvider>
</RecoilRoot>
</Hydrate>
<ReactQueryDevtools initialIsOpen={false} />
<AdminStatusProvider>
<Hydrate state={pageProps.dehydratedState}>
<RecoilRoot>
<ThemeProvider theme={theme}>
<Global styles={global} />
<Layout>
<Component {...pageProps} />
</Layout>
</ThemeProvider>
</RecoilRoot>
</Hydrate>
<ReactQueryDevtools initialIsOpen={false} />
</AdminStatusProvider>
</QueryClientProvider>
</>
);
Expand Down
5 changes: 4 additions & 1 deletion src/pages/attendanceAdmin/session/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Select from '@/components/session/Select';
import { PAGE_SIZE } from '@/data/queryData';
import { attendanceInit, attendanceOptions } from '@/data/sessionData';
import useObserver from '@/hooks/useObserver';
import { useUnauthorizedStatus } from '@/hooks/useUnauthorizedStatus';
import {
updateMemberAttendStatus,
updateMemberScore,
Expand Down Expand Up @@ -51,6 +52,8 @@ function SessionDetailPage() {
const [modal, setModal] = useState<number | null>(null);
const bottomRef: RefObject<HTMLDivElement> = useRef(null);

useUnauthorizedStatus('MAKERS');

const {
data: session,
isLoading: isLoadingSession,
Expand Down Expand Up @@ -246,7 +249,7 @@ function SessionDetailPage() {
return () => closeAttendance();
default:
// eslint-disable-next-line prettier/prettier
return () => { };
return () => {};
}
};

Expand Down
6 changes: 4 additions & 2 deletions src/pages/attendanceAdmin/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { useEffect, useState } from 'react';

import CreateSessionModal from '@/components/attendanceAdmin/session/CreateSessionModal';
import SessionList from '@/components/attendanceAdmin/session/SessionList';
import SessionListFooter from '@/components/attendanceAdmin/session/SessionListFooter';
import FloatingButton from '@/components/common/FloatingButton';
import Footer from '@/components/common/Footer';
import Modal from '@/components/common/modal';
import { useUnauthorizedStatus } from '@/hooks/useUnauthorizedStatus';

function SessionPage() {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);

useUnauthorizedStatus('MAKERS');

useEffect(() => {
if (isModalOpen) {
document.body.style.overflow = 'hidden';
Expand Down
3 changes: 3 additions & 0 deletions src/pages/attendanceAdmin/totalScore/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import MemberList from '@/components/attendanceAdmin/totalScore/MemberList';
import { useUnauthorizedStatus } from '@/hooks/useUnauthorizedStatus';

function TotalScorePage() {
useUnauthorizedStatus('MAKERS');

return <MemberList />;
}

Expand Down
13 changes: 9 additions & 4 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ import { colors } from '@sopt-makers/colors';
import { fonts } from '@sopt-makers/fonts';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import { useSetRecoilState } from 'recoil';

import { SoptMainLogo } from '@/assets/icons/SoptLogos';
import Button from '@/components/common/Button';
import Input from '@/components/common/Input';
import Loading from '@/components/common/Loading';
import OptionTemplate from '@/components/common/OptionTemplate';
import { adminStatusContext } from '@/components/devTools/AdminContextProvider';
import useInput from '@/hooks/useInput';
import { userLogin } from '@/services/api/auth';
import { user as userState } from '@/store/globalStore';
Expand All @@ -20,6 +19,7 @@ function LoginPage() {
const router = useRouter();

const setUser = useSetRecoilState(userState);
const { status, setStatus } = useContext(adminStatusContext);

const { state, onChange } = useInput<LoginData>({
email: '',
Expand All @@ -44,7 +44,12 @@ function LoginPage() {
setError({ status: true, message: result.message });
} else {
setUser(result);
router.replace('/attendanceAdmin/session');
setStatus(result.adminStatus);
router.replace(
result.adminStatus !== 'MAKERS'
? '/attendanceAdmin/session'
: '/alarmAdmin',
);
}
}
};
Expand Down
3 changes: 2 additions & 1 deletion src/store/globalStore.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { atom } from 'recoil';
import { atom, DefaultValue, selector } from 'recoil';

export const user = atom<User>({
key: 'userData',
default: {
id: 0,
adminStatus: 'NOT_CERTIFIED',
name: '',
},
});
7 changes: 7 additions & 0 deletions src/types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ declare global {
type SESSION_STATUS = 'BEFORE' | 'FIRST' | 'SECOND' | 'END';
type AlarmDropdownType = 'part' | 'target' | 'generation' | 'targetSelector';
type ALARM_STATUS = '전체' | '발송 전' | '발송 후';
type ADMIN_STATUS =
| 'SUPER_USER'
| 'SOPT'
| 'MAKERS'
| 'NOT_CERTIFIED'
| 'DEVELOPER';

/* 에러 */
interface LoginError {
Expand Down Expand Up @@ -141,6 +147,7 @@ declare global {
/* 어드민 */
interface User {
id: number;
adminStatus: ADMIN_STATUS;
name: string;
}

Expand Down

0 comments on commit 38dfe91

Please sign in to comment.