소스 검색

add project access right

tags/Baseline_30082024_FRONTEND_UAT
cyril.tsui 1 년 전
부모
커밋
ac910b671b
18개의 변경된 파일105개의 추가작업 그리고 19개의 파일을 삭제
  1. +17
    -0
      src/app/(main)/projects/create/not-found.tsx
  2. +9
    -0
      src/app/(main)/projects/create/page.tsx
  3. +7
    -0
      src/app/(main)/projects/createSub/page.tsx
  4. +4
    -1
      src/app/(main)/projects/edit/page.tsx
  5. +4
    -1
      src/app/(main)/projects/editSub/page.tsx
  6. +17
    -0
      src/app/(main)/projects/not-found.tsx
  7. +9
    -3
      src/app/(main)/projects/page.tsx
  8. +9
    -3
      src/app/utils/commonUtil.ts
  9. +1
    -1
      src/app/utils/fetchUtil.ts
  10. +1
    -1
      src/components/AppBar/AppBar.tsx
  11. +8
    -5
      src/components/CreateProject/CreateProject.tsx
  12. +4
    -0
      src/components/CreateProject/CreateProjectWrapper.tsx
  13. +1
    -0
      src/components/GenerateProjectCashFlowReport/GenerateProjectCashFlowReport.tsx
  14. +4
    -1
      src/components/ProjectSearch/ProjectSearch.tsx
  15. +4
    -1
      src/components/ProjectSearch/ProjectSearchWrapper.tsx
  16. +2
    -0
      src/components/SearchResults/SearchResults.tsx
  17. +1
    -1
      src/config/authConfig.ts
  18. +3
    -1
      src/middleware.ts

+ 17
- 0
src/app/(main)/projects/create/not-found.tsx 파일 보기

@@ -0,0 +1,17 @@
import { getServerI18n } from "@/i18n";
import { Stack, Typography, Link } from "@mui/material";
import NextLink from "next/link";

export default async function NotFound() {
const { t } = await getServerI18n("projects", "common");

return (
<Stack spacing={2}>
<Typography variant="h4">{t("Not Found")}</Typography>
<Typography variant="body1">{t("The create project page was not found!")}</Typography>
<Link href="/projects" component={NextLink} variant="body2">
{t("Return to all projects")}
</Link>
</Stack>
);
}

+ 9
- 0
src/app/(main)/projects/create/page.tsx 파일 보기

@@ -11,10 +11,13 @@ import {
} from "@/app/api/projects";
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import { getUserAbilities } from "@/app/utils/commonUtil";
import CreateProject from "@/components/CreateProject";
import { I18nProvider, getServerI18n } from "@/i18n";
import { MAINTAIN_PROJECT } from "@/middleware";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
import { notFound } from "next/navigation";

export const metadata: Metadata = {
title: "Create Project",
@@ -23,6 +26,12 @@ export const metadata: Metadata = {
const Projects: React.FC = async () => {
const { t } = await getServerI18n("projects");

const abilities = await getUserAbilities()

if (!abilities.includes(MAINTAIN_PROJECT)) {
notFound();
}

// Preload necessary dependencies
fetchAllTasks();
fetchTaskTemplates();


+ 7
- 0
src/app/(main)/projects/createSub/page.tsx 파일 보기

@@ -13,9 +13,11 @@ import {
} from "@/app/api/projects";
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import { getUserAbilities } from "@/app/utils/commonUtil";
import { ServerFetchError } from "@/app/utils/fetchUtil";
import CreateProject from "@/components/CreateProject";
import { I18nProvider, getServerI18n } from "@/i18n";
import { MAINTAIN_PROJECT } from "@/middleware";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
import { notFound } from "next/navigation";
@@ -26,6 +28,11 @@ export const metadata: Metadata = {

const Projects: React.FC = async () => {
const { t } = await getServerI18n("projects");
const abilities = await getUserAbilities()
if (!abilities.includes(MAINTAIN_PROJECT)) {
notFound();
}

// Preload necessary dependencies
fetchAllTasks();


+ 4
- 1
src/app/(main)/projects/edit/page.tsx 파일 보기

@@ -12,9 +12,11 @@ import {
} from "@/app/api/projects";
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import { getUserAbilities } from "@/app/utils/commonUtil";
import { ServerFetchError } from "@/app/utils/fetchUtil";
import CreateProject from "@/components/CreateProject";
import { I18nProvider, getServerI18n } from "@/i18n";
import { MAINTAIN_PROJECT } from "@/middleware";
import Typography from "@mui/material/Typography";
import { isArray } from "lodash";
import { Metadata } from "next";
@@ -32,8 +34,9 @@ const Projects: React.FC<Props> = async ({ searchParams }) => {
const { t } = await getServerI18n("projects");
// Assume projectId is string here
const projectId = searchParams["id"];
const abilities = await getUserAbilities()

if (!projectId || isArray(projectId)) {
if (!projectId || isArray(projectId) || abilities.includes(MAINTAIN_PROJECT)) {
notFound();
}



+ 4
- 1
src/app/(main)/projects/editSub/page.tsx 파일 보기

@@ -13,8 +13,10 @@ import {
} from "@/app/api/projects";
import { preloadStaff, preloadTeamLeads } from "@/app/api/staff";
import { fetchAllTasks, fetchTaskTemplates } from "@/app/api/tasks";
import { getUserAbilities } from "@/app/utils/commonUtil";
import CreateProject from "@/components/CreateProject";
import { I18nProvider, getServerI18n } from "@/i18n";
import { MAINTAIN_PROJECT } from "@/middleware";
import Typography from "@mui/material/Typography";
import { isArray } from "lodash";
import { Metadata } from "next";
@@ -32,7 +34,8 @@ const Projects: React.FC<Props> = async ({ searchParams }) => {
const { t } = await getServerI18n("projects");
const projectId = searchParams["id"];

if (!projectId || isArray(projectId)) {
const abilities = await getUserAbilities()
if (!projectId || isArray(projectId) || !abilities.includes(MAINTAIN_PROJECT)) {
notFound();
}



+ 17
- 0
src/app/(main)/projects/not-found.tsx 파일 보기

@@ -0,0 +1,17 @@
import { getServerI18n } from "@/i18n";
import { Stack, Typography, Link } from "@mui/material";
import NextLink from "next/link";

export default async function NotFound() {
const { t } = await getServerI18n("projects", "common");

return (
<Stack spacing={2}>
<Typography variant="h4">{t("Not Found")}</Typography>
<Typography variant="body1">{t("The project page was not found!")}</Typography>
<Link href="/home" component={NextLink} variant="body2">
{t("Return to home")}
</Link>
</Stack>
);
}

+ 9
- 3
src/app/(main)/projects/page.tsx 파일 보기

@@ -1,13 +1,15 @@
import { fetchProjectCategories, fetchProjects, preloadProjects } from "@/app/api/projects";
import { getUserAbilities } from "@/app/utils/commonUtil";
import ProjectSearch from "@/components/ProjectSearch";
import { getServerI18n } from "@/i18n";
import { MAINTAIN_PROJECT, VIEW_PROJECT } from "@/middleware";
import Add from "@mui/icons-material/Add";
import { ButtonGroup } from "@mui/material";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Suspense } from "react";

export const metadata: Metadata = {
@@ -19,6 +21,10 @@ const Projects: React.FC = async () => {
// preloadProjects();
fetchProjectCategories();
const projects = await fetchProjects();
const abilities = await getUserAbilities()
if (!abilities.includes(VIEW_PROJECT)) {
notFound();
}

return (
<>
@@ -31,7 +37,7 @@ const Projects: React.FC = async () => {
<Typography variant="h4" marginInlineEnd={2}>
{t("Projects")}
</Typography>
<Stack
{abilities.includes(MAINTAIN_PROJECT) && <Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
@@ -55,7 +61,7 @@ const Projects: React.FC = async () => {
>
{t("Create Project")}
</Button>
</Stack >
</Stack >}
</Stack>
<Suspense fallback={<ProjectSearch.Loading />}>
<ProjectSearch />


+ 9
- 3
src/app/utils/commonUtil.ts 파일 보기

@@ -1,8 +1,9 @@
import { SessionWithTokens, authOptions } from "@/config/authConfig"
import { getServerSession } from "next-auth"
export interface WildCard {
[key: string]: any;
}


export const dateInRange = (currentDate: string, startDate: string, endDate: string) => {

if (currentDate === undefined) {
@@ -46,6 +47,11 @@ export function readIntFromString(input: string): [string, number | null] | stri
const stringPart = parts.slice(0, parts.length - 1).join("-");
const intPartStr = parts[parts.length - 1];
const intPart = intPartStr ? parseInt(intPartStr, 10) : null;
return [stringPart, intPart];
}
}

export const getUserAbilities = async () => {
const session = await getServerSession(authOptions) as SessionWithTokens;
return session?.abilities ?? []
}

+ 1
- 1
src/app/utils/fetchUtil.ts 파일 보기

@@ -22,7 +22,7 @@ export const serverFetch: typeof fetch = async (input, init) => {
const session = await getServerSession<any, SessionWithTokens>(authOptions);
const accessToken = session?.accessToken;

console.log(accessToken);
// console.log(accessToken);
return fetch(input, {
...init,
headers: {


+ 1
- 1
src/components/AppBar/AppBar.tsx 파일 보기

@@ -16,7 +16,7 @@ export interface AppBarProps {
const AppBar: React.FC<AppBarProps> = async ({ avatarImageSrc, profileName }) => {
const session = await getServerSession(authOptions) as any;
const abilities: string[] = session.abilities
console.log(abilities)
// console.log(abilities)
return (
<I18nProvider namespaces={["common"]}>
<MUIAppBar position="sticky" color="default" elevation={4}>


+ 8
- 5
src/components/CreateProject/CreateProject.tsx 파일 보기

@@ -50,6 +50,7 @@ import {
successDialog,
} from "../Swal/CustomAlerts";
import dayjs from "dayjs";
import { DELETE_PROJECT } from "@/middleware";

export interface Props {
isEditMode: boolean;
@@ -70,6 +71,7 @@ export interface Props {
workNatures: WorkNature[];
allStaffs: StaffResult[];
grades: Grade[];
abilities: string[];
}

const hasErrorsInTab = (
@@ -113,6 +115,7 @@ const CreateProject: React.FC<Props> = ({
buildingTypes,
workNatures,
allStaffs,
abilities,
}) => {
const [serverError, setServerError] = useState("");
const [tabIndex, setTabIndex] = useState(0);
@@ -302,7 +305,7 @@ const CreateProject: React.FC<Props> = ({
{isEditMode && !(formProps.getValues("projectDeleted") === true) && (
<Stack direction="row" gap={1}>
{/* {!formProps.getValues("projectActualStart") && ( */}
{formProps.getValues("projectStatus").toLowerCase() === "pending to start" && (
{formProps.getValues("projectStatus")?.toLowerCase() === "pending to start" && (
<Button
name="start"
type="submit"
@@ -315,7 +318,7 @@ const CreateProject: React.FC<Props> = ({
)}
{/* {formProps.getValues("projectActualStart") &&
!formProps.getValues("projectActualEnd") && ( */}
{formProps.getValues("projectStatus").toLowerCase() === "on-going" && (
{formProps.getValues("projectStatus")?.toLowerCase() === "on-going" && (
<Button
name="complete"
type="submit"
@@ -329,9 +332,9 @@ const CreateProject: React.FC<Props> = ({
{!(
// formProps.getValues("projectActualStart") &&
// formProps.getValues("projectActualEnd")
formProps.getValues("projectStatus") === "Completed" ||
formProps.getValues("projectStatus") === "Deleted"
) && (
formProps.getValues("projectStatus")?.toLowerCase() === "completed" ||
formProps.getValues("projectStatus")?.toLowerCase() === "deleted"
) && abilities.includes(DELETE_PROJECT) && (
<Button
variant="outlined"
startIcon={<Delete />}


+ 4
- 0
src/components/CreateProject/CreateProjectWrapper.tsx 파일 보기

@@ -14,6 +14,7 @@ import {
import { fetchStaff, fetchTeamLeads } from "@/app/api/staff";
import { fetchAllCustomers, fetchAllSubsidiaries } from "@/app/api/customer";
import { fetchGrades } from "@/app/api/grades";
import { getUserAbilities } from "@/app/utils/commonUtil";

type CreateProjectProps = {
isEditMode: false;
@@ -43,6 +44,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => {
workNatures,
allStaffs,
grades,
abilities,
] = await Promise.all([
fetchAllTasks(),
fetchTaskTemplates(),
@@ -58,6 +60,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => {
fetchProjectWorkNatures(),
fetchStaff(),
fetchGrades(),
getUserAbilities(),
]);

const projectInfo = props.isEditMode
@@ -88,6 +91,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => {
allStaffs={allStaffs}
grades={grades}
mainProjects={mainProjects}
abilities={abilities}
/>
);
};


+ 1
- 0
src/components/GenerateProjectCashFlowReport/GenerateProjectCashFlowReport.tsx 파일 보기

@@ -36,6 +36,7 @@ const GenerateProjectCashFlowReport: React.FC<Props> = ({ projects }) => {
<SearchBox
criteria={searchCriteria}
onSearch={async (query) => {

if (Boolean(query.project) && query.project !== "All") {
// const projectIndex = projectCombo.findIndex(({value}) => value === parseInt(query.project))
const response = await fetchProjectCashFlowReport({ projectId: parseInt(query.project), dateType: query.dateType })


+ 4
- 1
src/components/ProjectSearch/ProjectSearch.tsx 파일 보기

@@ -8,16 +8,18 @@ import SearchResults, { Column } from "../SearchResults";
import EditNote from "@mui/icons-material/EditNote";
import uniq from "lodash/uniq";
import { useRouter } from "next/navigation";
import { MAINTAIN_PROJECT } from "@/middleware";

interface Props {
projects: ProjectResult[];
projectCategories: ProjectCategory[];
abilities: string[]
}

type SearchQuery = Partial<Omit<ProjectResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => {
const ProjectSearch: React.FC<Props> = ({ projects, projectCategories, abilities }) => {
const router = useRouter();
const { t } = useTranslation("projects");

@@ -75,6 +77,7 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => {
label: t("Details"),
onClick: onProjectClick,
buttonIcon: <EditNote />,
disabled: !abilities.includes(MAINTAIN_PROJECT),
},
{ name: "code", label: t("Project Code") },
{ name: "name", label: t("Project Name") },


+ 4
- 1
src/components/ProjectSearch/ProjectSearchWrapper.tsx 파일 보기

@@ -2,6 +2,7 @@ import { fetchProjectCategories, fetchProjects } from "@/app/api/projects";
import React from "react";
import ProjectSearch from "./ProjectSearch";
import ProjectSearchLoading from "./ProjectSearchLoading";
import { getUserAbilities } from "@/app/utils/commonUtil";

interface SubComponents {
Loading: typeof ProjectSearchLoading;
@@ -11,7 +12,9 @@ const ProjectSearchWrapper: React.FC & SubComponents = async () => {
const projectCategories = await fetchProjectCategories();
const projects = await fetchProjects();

return <ProjectSearch projects={projects} projectCategories={projectCategories} />;
const abilities = await getUserAbilities()

return <ProjectSearch projects={projects} projectCategories={projectCategories} abilities={abilities}/>;
};

ProjectSearchWrapper.Loading = ProjectSearchLoading;


+ 2
- 0
src/components/SearchResults/SearchResults.tsx 파일 보기

@@ -32,6 +32,7 @@ interface BaseColumn<T extends ResultWithId> {
interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> {
onClick: (item: T) => void;
buttonIcon: React.ReactNode;
disabled?: boolean;
}

export type Column<T extends ResultWithId> =
@@ -101,6 +102,7 @@ function SearchResults<T extends ResultWithId>({
<IconButton
color={column.color ?? "primary"}
onClick={() => column.onClick(item)}
disabled={Boolean(column.disabled)}
>
{column.buttonIcon}
</IconButton>


+ 1
- 1
src/config/authConfig.ts 파일 보기

@@ -5,7 +5,7 @@ import { LOGIN_API_PATH } from "./api";
export interface SessionWithTokens extends Session {
staff?: any;
role?: String;
abilities?: any[];
abilities?: string[];
accessToken?: string;
refreshToken?: string;
}


+ 3
- 1
src/middleware.ts 파일 보기

@@ -38,6 +38,7 @@ export const [
MAINTAIN_TIMESHEET_7DAYS,
VIEW_PROJECT,
MAINTAIN_PROJECT,
DELETE_PROJECT,
] = [
'VIEW_USER',
'MAINTAIN_USER',
@@ -56,7 +57,8 @@ export const [
'MAINTAIN_TASK_TEMPLATE',
'MAINTAIN_TIMESHEET_7DAYS',
'VIEW_PROJECT',
'MAINTAIN_PROJECT'
'MAINTAIN_PROJECT',
'DELETE_PROJECT'
]

const PRIVATE_ROUTES = [


불러오는 중...
취소
저장