Переглянути джерело

認証関連 整備

develop
sosuke.iwabuchi 2 роки тому
джерело
коміт
5c0a8b984d
12 змінених файлів з 270 додано та 104 видалено
  1. +9
    -0
      src/codes/page.ts
  2. +28
    -5
      src/contexts/AuthContext.tsx
  3. +1
    -2
      src/layouts/dashbord/index.tsx
  4. +63
    -56
      src/layouts/dashbord/navigator.tsx
  5. +1
    -1
      src/pages/auth/login.tsx
  6. +5
    -0
      src/pages/common/Page403.tsx
  7. +1
    -1
      src/pages/common/Page404.tsx
  8. +17
    -0
      src/pages/dashboard/index.tsx
  9. +17
    -0
      src/pages/dashboard/receipt-issuing-order/create.tsx
  10. +54
    -0
      src/routes/auth.ts
  11. +56
    -14
      src/routes/index.tsx
  12. +18
    -25
      src/routes/path.ts

+ 9
- 0
src/codes/page.ts Переглянути файл

@@ -5,8 +5,17 @@ export const PageID = {
LOGIN: id++,
LOGOUT: id++,

DASHBOARD_OVERVIEW: id++,

DASHBOARD_CONTRACT_LIST: id++,
DASHBOARD_CONTRACT_DETAIL: id++,

DASHBOARD_RECEIPT_ISSUING_ORDER_CREATE: id++,
DASHBOARD_RECEIPT_ISSUING_ORDER_LIST: id++,
DASHBOARD_RECEIPT_ISSUING_ORDER_DETAIL: id++,

PAGE_403: id++,
PAGE_404: id++,
} as const;

export type PageID = (typeof PageID)[keyof typeof PageID];


+ 28
- 5
src/contexts/AuthContext.tsx Переглянути файл

@@ -1,9 +1,18 @@
import { HasChildren } from "@types";
import { ResultCode } from "api";
import { login as APILogin, logout as APILogout, me } from "api/auth";
import { PageID } from "codes/page";
import { UserRole } from "codes/user";
import useAPICall from "hooks/useAPICall";
import { createContext, memo, useEffect, useMemo, useState } from "react";
import {
createContext,
memo,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { AUTH } from "routes/auth";

type Auth = {
initialized: boolean;
@@ -19,6 +28,7 @@ type Auth = {
changeContractId: (contractId: string) => Promise<boolean>;

checkRole: (role?: UserRole) => boolean;
canAccess: (pageId: PageID) => boolean;
};
export const AuthContext = createContext<Auth>({
initialized: false,
@@ -32,6 +42,7 @@ export const AuthContext = createContext<Auth>({
logout: () => {},
changeContractId: async (contractId: string) => false,
checkRole: (role?: UserRole) => false,
canAccess: (pageId: PageID) => false,
});

type Props = HasChildren;
@@ -93,10 +104,21 @@ function AuthContextProvider({ children }: Props) {
return false;
};

const checkRole = (targetRole?: UserRole): boolean => {
if (targetRole === undefined) return true;
return targetRole <= role;
};
const checkRole = useCallback(
(targetRole?: UserRole): boolean => {
if (targetRole === undefined) return true;
return targetRole <= role;
},
[role]
);

const canAccess = useCallback(
(pageId: PageID): boolean => {
const roles = AUTH[pageId] ?? [];
return roles.includes(role);
},
[role]
);

useEffect(() => {
callMe({});
@@ -116,6 +138,7 @@ function AuthContextProvider({ children }: Props) {
logout,
changeContractId,
checkRole,
canAccess,
}}
>
{children}


+ 1
- 2
src/layouts/dashbord/index.tsx Переглянути файл

@@ -23,8 +23,7 @@ function Copyright() {
function App() {
const [mobileOpen, setMobileOpen] = useState(false);

const { drawerWidth, innerHeight, innerWidth, contentsWidth, showDrawer } =
useDashboard();
const { drawerWidth, innerWidth, contentsWidth, showDrawer } = useDashboard();

const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);


+ 63
- 56
src/layouts/dashbord/navigator.tsx Переглянути файл

@@ -12,7 +12,6 @@ import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import { PageID } from "codes/page";
import { UserRole } from "codes/user";
import useAuth from "hooks/useAuth";
import useNavigateCustom from "hooks/useNavigateCustom";
import usePage from "hooks/usePage";
@@ -22,7 +21,6 @@ import { getPath } from "routes/path";
type Group = {
label: string;
children: SubGroup[];
role?: UserRole;
};

type SubGroup = {
@@ -32,13 +30,11 @@ type SubGroup = {

// 子要素を持たない場合は設定
id?: PageID;
role?: UserRole;
};

type Child = {
label: string;
id: PageID;
role?: UserRole;
};

const categories: Group[] = [
@@ -48,7 +44,6 @@ const categories: Group[] = [
{
label: "契約",
icon: <PeopleIcon />,
role: UserRole.SUPER_ADMIN,
children: [
{
id: PageID.DASHBOARD_CONTRACT_LIST,
@@ -60,6 +55,20 @@ const categories: Group[] = [
},
],
},
{
label: "領収証発行依頼",
icon: <PeopleIcon />,
children: [
{
id: PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_LIST,
label: "一覧",
},
{
id: PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_CREATE,
label: "新規",
},
],
},
],
},
{
@@ -114,23 +123,24 @@ export default function Navigator(props: DrawerProps) {
function Group(group: Group) {
const { label, children } = group;

const { checkRole } = useAuth();
if (!checkRole(group.role)) return null;
const elements = children.map((ele, index) => (
<SubGroup {...ele} key={index} />
));

if (elements.length === 0) return null;

return (
<Box sx={{ bgcolor: "#101F33" }}>
<ListItem sx={{ py: 2, px: 3 }}>
<ListItemText sx={{ color: "#fff" }}>{label}</ListItemText>
</ListItem>
{children.map((ele, index) => (
<SubGroup {...ele} key={index} />
))}
{elements}
<Divider sx={{ mt: 2 }} />
</Box>
);
}

function SubGroup({ icon, label, id, children, role }: SubGroup) {
function SubGroup({ icon, label, id, children }: SubGroup) {
const { pageId } = usePage();
const { navigateWhenChanged } = useNavigateCustom();

@@ -138,20 +148,15 @@ function SubGroup({ icon, label, id, children, role }: SubGroup) {

const [open, setOpen] = React.useState(false);

const { checkRole } = useAuth();

React.useEffect(() => {
setOpen(shouldOpen);
}, [shouldOpen]);

if (!checkRole(role)) return null;

// 子要素ありの場合
if (elements && elements.length !== 0) {
const handleClick = () => {
setOpen(!open);
};

return (
<>
<ListItemButton onClick={handleClick} sx={item} selected={false}>
@@ -167,57 +172,59 @@ function SubGroup({ icon, label, id, children, role }: SubGroup) {
</>
);
}

// 子要素なしの場合
const handleClick = () => {
if (id) {
const path = getPath(id);
navigateWhenChanged(path);
}
};
const selected = id === pageId;
return (
<ListItemButton onClick={handleClick} selected={selected} sx={item}>
<ListItemIcon>{icon}</ListItemIcon>
<ListItemText>{label}</ListItemText>
</ListItemButton>
);
if (id !== undefined) {
const handleClick = () => {
if (id) {
const path = getPath(id);
navigateWhenChanged(path);
}
};
const selected = id === pageId;
return (
<ListItemButton onClick={handleClick} selected={selected} sx={item}>
<ListItemIcon>{icon}</ListItemIcon>
<ListItemText>{label}</ListItemText>
</ListItemButton>
);
}
return null;
}

function useContents(children: Child[]) {
const { pageId } = usePage();
const { navigateWhenChanged } = useNavigateCustom();
const { checkRole } = useAuth();
const { canAccess, initialized } = useAuth();

const [shouldOpen, setShouldOpen] = React.useState(false);

const elements = React.useMemo(() => {
setShouldOpen(false);
return children.map(({ label, id, role }, index) => {
if (!checkRole(role)) return;
const selected = id === pageId;
if (selected) {
setShouldOpen(true);
}
const handleClick = () => {
const path = getPath(id);
navigateWhenChanged(path);
};
return (
<ListItemButton
selected={selected}
sx={{ ...item, pl: 4 }}
key={index}
onClick={handleClick}
>
<ListItemText primary={label} />
</ListItemButton>
);
});
}, [pageId]);
return children
.filter(({ id }) => canAccess(id))
.map(({ label, id }, index) => {
const selected = id === pageId;
if (selected) {
setShouldOpen(true);
}
const handleClick = () => {
const path = getPath(id);
navigateWhenChanged(path);
};
return (
<ListItemButton
selected={selected}
sx={{ ...item, pl: 4 }}
key={index}
onClick={handleClick}
>
<ListItemText primary={label} />
</ListItemButton>
);
});
}, [pageId, initialized]);

return {
elements,


+ 1
- 1
src/pages/auth/login.tsx Переглянути файл

@@ -49,7 +49,7 @@ export default function Login() {

if (ret) {
success("ログイン成功");
navigateWhenChanged(getPath(PageID.DASHBOARD_CONTRACT_LIST));
navigateWhenChanged(getPath(PageID.DASHBOARD_OVERVIEW));
} else {
error("ログイン失敗");
setMessage("入力情報を確認してください");


+ 5
- 0
src/pages/common/Page403.tsx Переглянути файл

@@ -0,0 +1,5 @@
import { Box } from "@mui/material";

export default function Page403() {
return <Box>Un Authenticated.</Box>;
}

+ 1
- 1
src/pages/common/Page404.tsx Переглянути файл

@@ -1,5 +1,5 @@
import { Box } from "@mui/material";

export default function TestB() {
export default function Page404() {
return <Box>NotFound.</Box>;
}

+ 17
- 0
src/pages/dashboard/index.tsx Переглянути файл

@@ -0,0 +1,17 @@
import { Box } from "@mui/material";
import { PageID, TabID } from "codes/page";
import useDashboard from "hooks/useDashBoard";
import { useEffect } from "react";

export default function Overview() {
const { setHeaderTitle, setTabs } = useDashboard(
PageID.DASHBOARD_OVERVIEW,
TabID.NONE
);

useEffect(() => {
setHeaderTitle("Dashboard");
setTabs(null);
}, []);
return <Box></Box>;
}

+ 17
- 0
src/pages/dashboard/receipt-issuing-order/create.tsx Переглянути файл

@@ -0,0 +1,17 @@
import { Box } from "@mui/material";
import { PageID, TabID } from "codes/page";
import useDashboard from "hooks/useDashBoard";
import { useEffect } from "react";

export default function ReceiptIssuingOrderCreate() {
const { setHeaderTitle, setTabs } = useDashboard(
PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_CREATE,
TabID.NONE
);

useEffect(() => {
setHeaderTitle("領収証発行依頼作成");
setTabs(null);
}, []);
return <Box>Create</Box>;
}

+ 54
- 0
src/routes/auth.ts Переглянути файл

@@ -0,0 +1,54 @@
import { PageID as P } from "codes/page";
import { UserRole as R } from "codes/user";

export const AUTH = {
[P.NONE]: setAuth("all"),
[P.LOGIN]: setAuth("all"),
[P.LOGOUT]: setAuth("all"),

[P.DASHBOARD_OVERVIEW]: setAuth("ge", R.NORMAL_ADMIN),

[P.DASHBOARD_CONTRACT_LIST]: setAuth("ge", R.SUPER_ADMIN),
[P.DASHBOARD_CONTRACT_DETAIL]: setAuth("ge", R.SUPER_ADMIN),

[P.DASHBOARD_RECEIPT_ISSUING_ORDER_CREATE]: setAuth("ge", R.NORMAL_ADMIN),
[P.DASHBOARD_RECEIPT_ISSUING_ORDER_LIST]: setAuth("ge", R.NORMAL_ADMIN),
[P.DASHBOARD_RECEIPT_ISSUING_ORDER_DETAIL]: setAuth("ge", R.NORMAL_ADMIN),

[P.PAGE_403]: setAuth("all"),
[P.PAGE_404]: setAuth("all"),
};

type Target = "ge" | "le" | "eq" | "all";
type UserRoleKey = keyof typeof R;
function setAuth(target: Target, targetRole?: R): R[] {
const ret: R[] = [];

for (const key in R) {
const role = R[key as UserRoleKey];

if (target === "all") {
ret.push(role);
continue;
}

if (targetRole === undefined) {
continue;
}

if (target === "ge" && role >= targetRole) {
ret.push(role);
continue;
}
if (target === "le" && role <= targetRole) {
ret.push(role);
continue;
}
if (target === "eq" && role === targetRole) {
ret.push(role);
continue;
}
}

return ret;
}

+ 56
- 14
src/routes/index.tsx Переглянути файл

@@ -1,10 +1,12 @@
import { PageID } from "codes/page";
import LoadingScreen from "components/LoadingScreen";
import DashboardLayout from "layouts/dashbord";
import { ElementType, Suspense, lazy } from "react";
import { RouteObject, useLocation, useRoutes } from "react-router-dom";
import { PATH, getRoute } from "./path";
import { PageID } from "codes/page";
import SimpleLayout from "layouts/simple";
import { ElementType, Suspense, lazy, useMemo } from "react";
import { RouteObject, useRoutes } from "react-router-dom";
import { getRoute } from "./path";
import useAuth from "hooks/useAuth";
import { UserRole } from "codes/user";

const Loadable = (Component: ElementType) => (props: any) => {
return (
@@ -28,41 +30,81 @@ const AuthRoutes = (): RouteObject => ({
],
});

const DashboardRoutes = (): RouteObject => ({
element: <DashboardLayout />,
children: [
const DashboardRoutes = (): RouteObject => {
const { canAccess } = useAuth();

const allChildren = [
{
path: getRoute(PageID.DASHBOARD_CONTRACT_LIST),
pageId: PageID.DASHBOARD_OVERVIEW,
element: <Dashboard />,
target: UserRole.NORMAL_ADMIN,
},
{
pageId: PageID.DASHBOARD_CONTRACT_LIST,
element: <ContractList />,
target: UserRole.SUPER_ADMIN,
},
{
path: getRoute(PageID.DASHBOARD_CONTRACT_DETAIL),
pageId: PageID.DASHBOARD_CONTRACT_DETAIL,
element: <ContractDetail />,
target: UserRole.SUPER_ADMIN,
},
],
});
{
pageId: PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_CREATE,
element: <ReceiptIssuingOrderCreate />,
target: UserRole.NORMAL_ADMIN,
},
];

const children: RouteObject[] = useMemo(() => {
return allChildren
.filter(({ pageId }) => canAccess(pageId))
.map(({ pageId, ...others }) => ({
...others,
path: getRoute(pageId),
}));
}, [canAccess]);

return {
element: <DashboardLayout />,
children,
};
};

export function Routes() {
const { initialized } = useAuth();
return useRoutes([
AuthRoutes(),
DashboardRoutes(),
{
path: "403",
element: <Page403 />,
},
{
path: "*",
element: <Page404 />,
element: initialized ? <Page404 /> : <LoadingScreen />,
},
]);
}

const TestAPage = Loadable(lazy(() => import("pages/test/TestA")));
const TestBPage = Loadable(lazy(() => import("pages/test/TestB")));
// 認証関連 -------------------------------
const Login = Loadable(lazy(() => import("pages/auth/login")));
const Logout = Loadable(lazy(() => import("pages/auth/logout")));

//ダッシュボード ----------------------------
const Dashboard = Loadable(lazy(() => import("pages/dashboard")));
// 契約関連
const ContractList = Loadable(
lazy(() => import("pages/dashboard/contract/list"))
);
const ContractDetail = Loadable(
lazy(() => import("pages/dashboard/contract/detail"))
);
// 領収証発行依頼
const ReceiptIssuingOrderCreate = Loadable(
lazy(() => import("pages/dashboard/receipt-issuing-order/create"))
);

// その他 ---------------------------------
const Page403 = Loadable(lazy(() => import("pages/common/Page403")));
const Page404 = Loadable(lazy(() => import("pages/common/Page404")));

+ 18
- 25
src/routes/path.ts Переглянути файл

@@ -1,30 +1,6 @@
import { Dictionary } from "@types";
import { PageID, TabID } from "codes/page";
import { get, isArray, isNumber, isString, replace } from "lodash";

const DASHBOARD = "dashboard";

export const PATH = {
login: "/login",
logout: "/logout",
dashboard: {
root: "/dashboard",
contract: "/contract",
},
};

// const makePath = (paths: string[]): string => {
// return "/" + paths.join("/");
// };

// const makeListPageCallback = (path: string) => {
// return (page: number) => {
// return [path, String(page)].join("/");
// };
// };
// const makeDashboardPath = (paths: string[]): string => {
// return makePath([PATH.dashboard.root, ...paths]);
// };
import { get, isArray, isString, replace } from "lodash";

type PathKey = [PageID, TabID?] | PageID;
const makePathKey = (arg: PathKey): string => {
@@ -53,13 +29,30 @@ const getTabId = (key: PathKey): TabID => {
};

const PATHS = {
[makePathKey(PageID.NONE)]: "/",

// 認証
[makePathKey(PageID.LOGIN)]: "/login",
[makePathKey(PageID.LOGOUT)]: "/logout",

[makePathKey(PageID.DASHBOARD_OVERVIEW)]: "/dashboard",

// 契約関連
[makePathKey(PageID.DASHBOARD_CONTRACT_LIST)]:
"/dashboard/contract/list/:page",
[makePathKey(PageID.DASHBOARD_CONTRACT_DETAIL)]: "/dashboard/contract/detail",

// 領収証発行依頼関連
[makePathKey(PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_CREATE)]:
"/dashboard/receipt-issusing-order/create",
[makePathKey(PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_LIST)]:
"/dashboard/receipt-issusing-order/list/:page",
[makePathKey(PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_DETAIL)]:
"/dashboard/receipt-issusing-order/detail",

// その他
[makePathKey(PageID.PAGE_403)]: "403",
[makePathKey(PageID.PAGE_404)]: "404",
};

export type PathOption = {


Завантаження…
Відмінити
Зберегти