├── .npmrc ├── src ├── domain │ ├── roles │ │ ├── api │ │ │ ├── index.ts │ │ │ ├── role.keys.ts │ │ │ └── getRoles.ts │ │ ├── index.ts │ │ └── features │ │ │ ├── index.ts │ │ │ ├── AssignedRolesList.tsx │ │ │ └── RolesForm.tsx │ ├── auth │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── useAuthUser.tsx │ │ ├── index.ts │ │ └── components │ │ │ ├── index.ts │ │ │ ├── Forbidden.tsx │ │ │ └── login.tsx │ ├── permissions │ │ ├── utils │ │ │ ├── index.ts │ │ │ └── permissions.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── usePermissionGuards.tsx │ │ ├── index.ts │ │ └── api │ │ │ ├── index.ts │ │ │ ├── permission.keys.ts │ │ │ ├── getPermissionsList.ts │ │ │ └── getCurrentUserPermissions.ts │ ├── users │ │ ├── features │ │ │ ├── index.ts │ │ │ ├── UserListTable.tsx │ │ │ └── UserForm.tsx │ │ ├── index.ts │ │ ├── api │ │ │ ├── index.ts │ │ │ ├── user.keys.ts │ │ │ ├── getUser.ts │ │ │ ├── deleteUser.ts │ │ │ ├── addUser.ts │ │ │ ├── removeUserRole.ts │ │ │ ├── updateUser.ts │ │ │ ├── addUserRole.ts │ │ │ └── getUserList.ts │ │ ├── validation.tsx │ │ └── types │ │ │ └── index.ts │ └── rolePermissions │ │ ├── features │ │ ├── index.ts │ │ ├── RolePermissionListTable.tsx │ │ └── RolePermissionForm.tsx │ │ ├── index.ts │ │ ├── validation.tsx │ │ ├── api │ │ ├── index.ts │ │ ├── rolePermission.keys.ts │ │ ├── getRolePermission.tsx │ │ ├── deleteRolePermission.tsx │ │ ├── addRolePermission.tsx │ │ ├── updateRolePermission.tsx │ │ └── getRolePermissionList.tsx │ │ └── types │ │ └── index.ts ├── components │ ├── notifications │ │ ├── index.ts │ │ └── Notifications.tsx │ ├── settings │ │ ├── index.ts │ │ ├── UsersTab.tsx │ │ └── RolePermissionsTab.tsx │ ├── index.ts │ ├── forms │ │ ├── index.ts │ │ ├── DebouncedInput.tsx │ │ ├── TrashButton.tsx │ │ ├── TextInput.tsx │ │ ├── Tabs.tsx │ │ ├── TextArea.tsx │ │ ├── Button.tsx │ │ ├── Checkbox.tsx │ │ ├── Autocomplete.tsx │ │ ├── NumberInput.tsx │ │ ├── Combobox.tsx │ │ ├── DatePicker.tsx │ │ └── PaginatedTable.tsx │ ├── PrivateLayout.tsx │ ├── SearchInput.tsx │ ├── modal │ │ └── ConfirmDeleteModal.tsx │ ├── PrivateHeader.tsx │ ├── PrivateSideNav.tsx │ └── ThemeToggle.tsx ├── types │ ├── index.ts │ ├── forms.ts │ ├── react-table.d.ts │ ├── next.d.ts │ └── apis.ts ├── utils │ ├── index.ts │ ├── forms.ts │ ├── sorting.ts │ ├── testing.ts │ └── Autosave │ │ ├── AutosaveMachine.typegen.ts │ │ ├── useAutosave.tsx │ │ └── AutosaveMachine.tsx ├── hooks │ ├── useIsomorphicLayoutEffect.tsx │ ├── useAutofocus.tsx │ └── useTailwindConfig.tsx ├── pages │ ├── token.tsx │ ├── _document.tsx │ ├── settings │ │ ├── users │ │ │ ├── new.tsx │ │ │ └── [userId].tsx │ │ └── index.tsx │ ├── _app.tsx │ ├── api │ │ └── auth │ │ │ └── [...nextauth].ts │ └── index.tsx ├── config │ └── index.ts ├── images │ ├── favicon.svg │ └── logo.svg ├── lib │ └── axios.tsx └── styles │ └── globals.css ├── .eslintrc.json ├── postcss.config.js ├── .env.example ├── next.config.js ├── .gitignore ├── tsconfig.json ├── package.json ├── public └── favicon.svg ├── tailwind.config.js └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true -------------------------------------------------------------------------------- /src/domain/roles/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getRoles"; 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/domain/auth/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useAuthUser"; 2 | -------------------------------------------------------------------------------- /src/components/notifications/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Notifications'; -------------------------------------------------------------------------------- /src/domain/permissions/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./permissions"; 2 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./apis"; 2 | export * from "./forms"; 3 | -------------------------------------------------------------------------------- /src/domain/permissions/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./usePermissionGuards"; 2 | -------------------------------------------------------------------------------- /src/domain/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components"; 2 | export * from "./hooks"; 3 | -------------------------------------------------------------------------------- /src/domain/roles/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | export * from "./features"; 3 | -------------------------------------------------------------------------------- /src/domain/auth/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Forbidden"; 2 | export * from "./Login"; 3 | -------------------------------------------------------------------------------- /src/domain/users/features/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./UserForm"; 2 | export * from "./UserListTable"; 3 | -------------------------------------------------------------------------------- /src/components/settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RolePermissionsTab"; 2 | export * from "./UsersTab"; 3 | -------------------------------------------------------------------------------- /src/domain/roles/features/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AssignedRolesList"; 2 | export * from "./RolesForm"; 3 | -------------------------------------------------------------------------------- /src/domain/permissions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | export * from "./hooks"; 3 | export * from "./utils"; 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/domain/permissions/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getCurrentUserPermissions"; 2 | export * from "./getPermissionsList"; 3 | -------------------------------------------------------------------------------- /src/domain/roles/api/role.keys.ts: -------------------------------------------------------------------------------- 1 | const RoleKeys = { 2 | all: ["Roles"] as const, 3 | }; 4 | 5 | export { RoleKeys }; 6 | -------------------------------------------------------------------------------- /src/domain/rolePermissions/features/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RolePermissionForm"; 2 | export * from "./RolePermissionListTable"; -------------------------------------------------------------------------------- /src/domain/auth/components/Forbidden.tsx: -------------------------------------------------------------------------------- 1 | function Forbidden() { 2 | return
Forbidden
; 3 | } 4 | 5 | export { Forbidden }; 6 | -------------------------------------------------------------------------------- /src/domain/users/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | export * from "./features"; 3 | export * from "./types"; 4 | export * from "./validation"; 5 | -------------------------------------------------------------------------------- /src/types/forms.ts: -------------------------------------------------------------------------------- 1 | export const FormControlState = ["valid", "invalid", "disabled"] as const; 2 | export const FormMode = ["Add", "Edit"] as const; 3 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Autosave/useAutosave"; 2 | export * from "./forms"; 3 | export * from "./sorting"; 4 | export * from "./testing"; 5 | -------------------------------------------------------------------------------- /src/domain/permissions/api/permission.keys.ts: -------------------------------------------------------------------------------- 1 | const PermissionKeys = { 2 | all: ["Permissions"] as const, 3 | }; 4 | 5 | export { PermissionKeys }; 6 | -------------------------------------------------------------------------------- /src/domain/rolePermissions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | export * from "./features"; 3 | export * from "./types"; 4 | export * from "./validation"; 5 | -------------------------------------------------------------------------------- /src/types/react-table.d.ts: -------------------------------------------------------------------------------- 1 | import "@tanstack/react-table"; 2 | 3 | declare module "@tanstack/react-table" { 4 | interface ColumnMeta { 5 | thClassName?: string; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXTAUTH_URL=http://localhost:8582 2 | NEXTAUTH_SECRET=your-secret-here 3 | AUTH_AUTHORITY=http://localhost:3255/auth/realms/DevRealm 4 | AUTH_CLIENT_ID=recipe_management.next 5 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./PrivateHeader"; 2 | export * from "./PrivateLayout"; 3 | export * from "./PrivateSideNav"; 4 | export * from "./SearchInput"; 5 | export * from "./ThemeToggle"; 6 | -------------------------------------------------------------------------------- /src/hooks/useIsomorphicLayoutEffect.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from "react"; 2 | 3 | export const useIsomorphicLayoutEffect = 4 | typeof window !== "undefined" ? useLayoutEffect : useEffect; 5 | -------------------------------------------------------------------------------- /src/domain/users/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./addUser"; 2 | export * from "./deleteUser"; 3 | export * from "./getUser"; 4 | export * from "./getUserList"; 5 | export * from "./updateUser"; 6 | export * from "./user.keys"; 7 | -------------------------------------------------------------------------------- /src/domain/rolePermissions/validation.tsx: -------------------------------------------------------------------------------- 1 | import * as yup from "yup"; 2 | 3 | export const rolePermissionValidationSchema = yup.object({ 4 | role: yup.string().required(), 5 | permission: yup.string().required(), 6 | }); 7 | -------------------------------------------------------------------------------- /src/types/next.d.ts: -------------------------------------------------------------------------------- 1 | NextComponentType; 2 | import "next/types"; 3 | 4 | declare module "next/types" { 5 | interface NextComponentType { 6 | isPublic: boolean; 7 | } 8 | interface NextPage { 9 | isPublic: boolean; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/forms.ts: -------------------------------------------------------------------------------- 1 | import { FieldNamesMarkedBoolean, FieldValues } from "react-hook-form"; 2 | 3 | export function getSimpleDirtyFields( 4 | dirtyFields: FieldNamesMarkedBoolean{JSON.stringify(session, null, 2)}
18 |27 | Are you sure you want to delete this record? 28 |
29 | ), 30 | labels: { confirm: "Delete", cancel: "Cancel" }, 31 | confirmProps: { 32 | classNames: { 33 | root: cx("bg-red-400 hover:bg-red-500"), 34 | }, 35 | }, 36 | //TODO make secondary button style?? 37 | cancelProps: { 38 | classNames: { 39 | root: cx( 40 | "bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-900 dark:text-white dark:hover:bg-slate-800" 41 | ), 42 | }, 43 | }, 44 | classNames: { 45 | modal: cx("bg-white dark:bg-slate-700"), 46 | title: cx("text-slate-900 dark:text-white text-lg font-medium"), 47 | close: cx( 48 | "bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-900 dark:text-white dark:hover:bg-slate-800" 49 | ), 50 | }, 51 | onCancel, 52 | onConfirm, 53 | }); 54 | return openDeleteModal; 55 | } 56 | 57 | export default useDeleteModal; 58 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { Notifications } from "@/components/notifications"; 2 | import { useAuthUser } from "@/domain/auth"; 3 | import Login from "@/domain/auth/components/Login"; 4 | import "@/styles/globals.css"; 5 | import { MantineProvider } from "@mantine/core"; 6 | import { ModalsProvider } from "@mantine/modals"; 7 | import { SessionProvider } from "next-auth/react"; 8 | import type { AppProps } from "next/app"; 9 | import { QueryClient, QueryClientProvider } from "react-query"; 10 | import { ReactQueryDevtools } from "react-query/devtools"; 11 | 12 | function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) { 13 | return ( 14 | <> 15 |{role}
39 | {canRemoveUserRole.hasPermission && ( 40 |{info.getValue()}
, 56 | header: () => Role, 57 | }), 58 | columnHelper.accessor((row) => row.permission, { 59 | id: "permission", 60 | cell: (info) =>{info.getValue()}
, 61 | header: () => Permission, 62 | }), 63 | columnHelper.accessor("id", { 64 | enableSorting: false, 65 | meta: { thClassName: "w-10" }, 66 | cell: (row) => ( 67 |{error}
} 92 | > 93 | ); 94 | } 95 | ); 96 | 97 | Checkbox.displayName = "Checkbox"; 98 | export { Checkbox }; 99 | -------------------------------------------------------------------------------- /src/components/forms/Autocomplete.tsx: -------------------------------------------------------------------------------- 1 | import { FormControlState } from "@/types"; 2 | import { getTestSelector } from "@/utils/testing"; 3 | import { 4 | Autocomplete as MantineAutocomplete, 5 | AutocompleteProps as MantineAutocompleteProps, 6 | createStyles, 7 | } from "@mantine/core"; 8 | import { IconAlertCircle } from "@tabler/icons"; 9 | import clsx from "clsx"; 10 | import { useTailwindColors } from "../../hooks/useTailwindConfig"; 11 | import { useSetting } from "../ThemeToggle"; 12 | 13 | interface AutocompleteProps extends MantineAutocompleteProps { 14 | testSelector: string; 15 | errorSrOnly?: boolean; 16 | } 17 | 18 | function Autocomplete({ 19 | testSelector, 20 | errorSrOnly, 21 | ...rest 22 | }: AutocompleteProps) { 23 | const themeSetting = useSetting((state) => state.setting); 24 | const twColors = useTailwindColors(); 25 | const useStyles = createStyles({ 26 | item: { 27 | color: 28 | themeSetting === "dark" 29 | ? twColors?.slate["400"] 30 | : twColors?.slate["700"], 31 | "&[data-hovered]": { 32 | color: 33 | themeSetting === "dark" 34 | ? twColors?.slate["100"] 35 | : twColors?.slate["600"], 36 | backgroundColor: 37 | themeSetting === "dark" 38 | ? twColors?.slate["600"] 39 | : twColors?.slate["200"], 40 | }, 41 | 42 | "&[data-selected]": { 43 | color: 44 | themeSetting === "dark" 45 | ? twColors?.violet["100"] 46 | : twColors?.violet["600"], 47 | backgroundColor: 48 | themeSetting === "dark" 49 | ? twColors?.violet["600"] 50 | : twColors?.violet["200"], 51 | }, 52 | 53 | "&[data-selected]&:hover": { 54 | color: 55 | themeSetting === "dark" 56 | ? twColors?.slate["100"] 57 | : twColors?.slate["600"], 58 | backgroundColor: 59 | themeSetting === "dark" 60 | ? twColors?.slate["600"] 61 | : twColors?.slate["200"], 62 | }, 63 | }, 64 | }); 65 | const { classes, cx } = useStyles(); 66 | const { error, disabled } = rest; 67 | 68 | let inputState = "valid" as typeof FormControlState[number]; 69 | if (error) inputState = "invalid"; 70 | if (disabled) inputState = "disabled"; 71 | 72 | return ( 73 |{info.getValue()}
, 58 | header: () => Identifier, 59 | }), 60 | columnHelper.accessor((row) => row.firstName, { 61 | id: "firstName", 62 | cell: (info) =>{info.getValue()}
, 63 | header: () => First Name, 64 | }), 65 | columnHelper.accessor((row) => row.lastName, { 66 | id: "lastName", 67 | cell: (info) =>{info.getValue()}
, 68 | header: () => Last Name, 69 | }), 70 | columnHelper.accessor((row) => row.email, { 71 | id: "email", 72 | cell: (info) =>{info.getValue()}
, 73 | header: () => Email, 74 | }), 75 | columnHelper.accessor((row) => row.username, { 76 | id: "username", 77 | cell: (info) =>{info.getValue()?.toLocaleString()}
, 78 | header: () => Username, 79 | }), 80 | columnHelper.accessor("id", { 81 | enableSorting: false, 82 | meta: { thClassName: "w-10" }, 83 | cell: (row) => ( 84 |{label}
79 | 80 |