Przeglądaj źródła

認証関連 整備

develop
sosuke.iwabuchi 2 lat temu
rodzic
commit
5c0a8b984d
12 zmienionych plików z 270 dodań i 104 usunięć
  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 Wyświetl plik

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


DASHBOARD_OVERVIEW: id++,

DASHBOARD_CONTRACT_LIST: id++, DASHBOARD_CONTRACT_LIST: id++,
DASHBOARD_CONTRACT_DETAIL: 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; } as const;


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


+ 28
- 5
src/contexts/AuthContext.tsx Wyświetl plik

@@ -1,9 +1,18 @@
import { HasChildren } from "@types"; import { HasChildren } from "@types";
import { ResultCode } from "api"; import { ResultCode } from "api";
import { login as APILogin, logout as APILogout, me } from "api/auth"; import { login as APILogin, logout as APILogout, me } from "api/auth";
import { PageID } from "codes/page";
import { UserRole } from "codes/user"; import { UserRole } from "codes/user";
import useAPICall from "hooks/useAPICall"; 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 = { type Auth = {
initialized: boolean; initialized: boolean;
@@ -19,6 +28,7 @@ type Auth = {
changeContractId: (contractId: string) => Promise<boolean>; changeContractId: (contractId: string) => Promise<boolean>;


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


type Props = HasChildren; type Props = HasChildren;
@@ -93,10 +104,21 @@ function AuthContextProvider({ children }: Props) {
return false; 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(() => { useEffect(() => {
callMe({}); callMe({});
@@ -116,6 +138,7 @@ function AuthContextProvider({ children }: Props) {
logout, logout,
changeContractId, changeContractId,
checkRole, checkRole,
canAccess,
}} }}
> >
{children} {children}


+ 1
- 2
src/layouts/dashbord/index.tsx Wyświetl plik

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


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


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


+ 63
- 56
src/layouts/dashbord/navigator.tsx Wyświetl plik

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


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


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


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


const categories: Group[] = [ const categories: Group[] = [
@@ -48,7 +44,6 @@ const categories: Group[] = [
{ {
label: "契約", label: "契約",
icon: <PeopleIcon />, icon: <PeopleIcon />,
role: UserRole.SUPER_ADMIN,
children: [ children: [
{ {
id: PageID.DASHBOARD_CONTRACT_LIST, 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) { function Group(group: Group) {
const { label, children } = 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 ( return (
<Box sx={{ bgcolor: "#101F33" }}> <Box sx={{ bgcolor: "#101F33" }}>
<ListItem sx={{ py: 2, px: 3 }}> <ListItem sx={{ py: 2, px: 3 }}>
<ListItemText sx={{ color: "#fff" }}>{label}</ListItemText> <ListItemText sx={{ color: "#fff" }}>{label}</ListItemText>
</ListItem> </ListItem>
{children.map((ele, index) => (
<SubGroup {...ele} key={index} />
))}
{elements}
<Divider sx={{ mt: 2 }} /> <Divider sx={{ mt: 2 }} />
</Box> </Box>
); );
} }


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


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


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


const { checkRole } = useAuth();

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


if (!checkRole(role)) return null;

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

return ( return (
<> <>
<ListItemButton onClick={handleClick} sx={item} selected={false}> <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[]) { function useContents(children: Child[]) {
const { pageId } = usePage(); const { pageId } = usePage();
const { navigateWhenChanged } = useNavigateCustom(); const { navigateWhenChanged } = useNavigateCustom();
const { checkRole } = useAuth();
const { canAccess, initialized } = useAuth();


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


const elements = React.useMemo(() => { const elements = React.useMemo(() => {
setShouldOpen(false); 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 { return {
elements, elements,


+ 1
- 1
src/pages/auth/login.tsx Wyświetl plik

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


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


+ 5
- 0
src/pages/common/Page403.tsx Wyświetl plik

@@ -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 Wyświetl plik

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


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

+ 17
- 0
src/pages/dashboard/index.tsx Wyświetl plik

@@ -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 Wyświetl plik

@@ -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 Wyświetl plik

@@ -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 Wyświetl plik

@@ -1,10 +1,12 @@
import { PageID } from "codes/page";
import LoadingScreen from "components/LoadingScreen"; import LoadingScreen from "components/LoadingScreen";
import DashboardLayout from "layouts/dashbord"; 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 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) => { const Loadable = (Component: ElementType) => (props: any) => {
return ( 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 />, element: <ContractList />,
target: UserRole.SUPER_ADMIN,
}, },
{ {
path: getRoute(PageID.DASHBOARD_CONTRACT_DETAIL),
pageId: PageID.DASHBOARD_CONTRACT_DETAIL,
element: <ContractDetail />, 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() { export function Routes() {
const { initialized } = useAuth();
return useRoutes([ return useRoutes([
AuthRoutes(), AuthRoutes(),
DashboardRoutes(), DashboardRoutes(),
{
path: "403",
element: <Page403 />,
},
{ {
path: "*", 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 Login = Loadable(lazy(() => import("pages/auth/login")));
const Logout = Loadable(lazy(() => import("pages/auth/logout"))); const Logout = Loadable(lazy(() => import("pages/auth/logout")));


//ダッシュボード ----------------------------
const Dashboard = Loadable(lazy(() => import("pages/dashboard")));
// 契約関連
const ContractList = Loadable( const ContractList = Loadable(
lazy(() => import("pages/dashboard/contract/list")) lazy(() => import("pages/dashboard/contract/list"))
); );
const ContractDetail = Loadable( const ContractDetail = Loadable(
lazy(() => import("pages/dashboard/contract/detail")) 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"))); const Page404 = Loadable(lazy(() => import("pages/common/Page404")));

+ 18
- 25
src/routes/path.ts Wyświetl plik

@@ -1,30 +1,6 @@
import { Dictionary } from "@types"; import { Dictionary } from "@types";
import { PageID, TabID } from "codes/page"; 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; type PathKey = [PageID, TabID?] | PageID;
const makePathKey = (arg: PathKey): string => { const makePathKey = (arg: PathKey): string => {
@@ -53,13 +29,30 @@ const getTabId = (key: PathKey): TabID => {
}; };


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

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


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

// 契約関連
[makePathKey(PageID.DASHBOARD_CONTRACT_LIST)]: [makePathKey(PageID.DASHBOARD_CONTRACT_LIST)]:
"/dashboard/contract/list/:page", "/dashboard/contract/list/:page",
[makePathKey(PageID.DASHBOARD_CONTRACT_DETAIL)]: "/dashboard/contract/detail", [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 = { export type PathOption = {


Ładowanie…
Anuluj
Zapisz