diff --git a/apps/server/src/api/controllers/auth/get-permissions-controller.ts b/apps/server/src/api/controllers/auth/get-permissions-controller.ts new file mode 100644 index 00000000..923ded35 --- /dev/null +++ b/apps/server/src/api/controllers/auth/get-permissions-controller.ts @@ -0,0 +1,13 @@ +import type { IHttp } from '@stocker/core/interfaces' +import { HTTP_STATUS_CODE } from '@stocker/core/constants' + +import { companiesRepository } from '@/database' +import { User } from '@stocker/core/entities' + +export class GetPermissionsController { + async handle(http: IHttp) { + const user = User.create(await http.getUser()) + const role = await companiesRepository.findRoleById(user.role, user.companyId) + return http.send(role?.permissions, HTTP_STATUS_CODE.ok) + } +} diff --git a/apps/server/src/api/controllers/auth/index.ts b/apps/server/src/api/controllers/auth/index.ts index bc061c67..5a7c3721 100644 --- a/apps/server/src/api/controllers/auth/index.ts +++ b/apps/server/src/api/controllers/auth/index.ts @@ -5,3 +5,4 @@ export { ConfirmAuthController } from './confirm-auth-controller' export { DeleteAccountController } from './delete-account-controller' export { ResetPasswordController } from './reset-password-controller' export { UpdateAccountController } from './update-account-controller' +export { GetPermissionsController } from './get-permissions-controller' diff --git a/apps/server/src/app/fastify/routes/auth-routes.ts b/apps/server/src/app/fastify/routes/auth-routes.ts index 561eb127..a42476a4 100644 --- a/apps/server/src/app/fastify/routes/auth-routes.ts +++ b/apps/server/src/app/fastify/routes/auth-routes.ts @@ -5,6 +5,7 @@ import { FastifyHandler } from '../fastify-handler' import { ConfirmAuthController, DeleteAccountController, + GetPermissionsController, LoginController, LogoutController, ResetPasswordController, @@ -23,6 +24,7 @@ export const AuthRoutes = async (app: FastifyInstance) => { const deleteAccountController = new DeleteAccountController() const updateAccountController = new UpdateAccountController() const requestPasswordResetController = new RequestPasswordResetController() + const getPermissionsController = new GetPermissionsController() const verifyJwtMiddleware = new FastifyHandler(new VerifyJwtMiddleware()) const verifyAdminRoleMiddleware = new FastifyHandler( new VerifyRolePermissionMiddleware('all'), @@ -39,6 +41,11 @@ export const AuthRoutes = async (app: FastifyInstance) => { ws.join(userId, socket) }) + app.get('/permissions', async (request, response) => { + const http = new FastifyHttp(request, response) + return getPermissionsController.handle(http) + }) + app.post('/confirm', async (request, response) => { const http = new FastifyHttp(request, response) return confirmAuthController.handle(http) diff --git a/apps/server/src/database/prisma/repositories/prisma-users-repository.ts b/apps/server/src/database/prisma/repositories/prisma-users-repository.ts index ab99e3aa..6a36f16b 100644 --- a/apps/server/src/database/prisma/repositories/prisma-users-repository.ts +++ b/apps/server/src/database/prisma/repositories/prisma-users-repository.ts @@ -15,6 +15,7 @@ export class PrismaUsersRepository implements IUsersRepository { const role = await prisma.role.findUnique({ where: { name: user.role } }) if (!role) return + const prismaRoles = await prisma.role.findMany() const prismaUser = this.mapper.toPrisma(user) await prisma.user.create({ data: { @@ -22,7 +23,9 @@ export class PrismaUsersRepository implements IUsersRepository { name: prismaUser.name, email: prismaUser.email, password: prismaUser.password, - role_id: role.id, + role_id: + prismaRoles.find((prismaRole) => prismaRole.name === prismaUser.role?.name) + ?.id ?? '', has_first_password_reset: prismaUser.has_first_password_reset, company_id: prismaUser.company_id, }, @@ -35,6 +38,8 @@ export class PrismaUsersRepository implements IUsersRepository { async addMany(users: User[]): Promise { try { const prismaUsers = users.map(this.mapper.toPrisma) + const prismaRoles = await prisma.role.findMany() + await prisma.user.createMany({ data: prismaUsers.map((prismaUser) => { return { @@ -42,7 +47,9 @@ export class PrismaUsersRepository implements IUsersRepository { name: prismaUser.name, email: prismaUser.email, password: prismaUser.password, - role_id: prismaUser.role_id, + role_id: + prismaRoles.find((prismaRole) => prismaRole.name === prismaUser.role?.name) + ?.id ?? '', has_first_password_reset: prismaUser.has_first_password_reset, company_id: prismaUser.company_id, } @@ -174,13 +181,17 @@ export class PrismaUsersRepository implements IUsersRepository { async update(user: User, userId: string): Promise { try { const prismaUser = this.mapper.toPrisma(user) + const prismaRoles = await prisma.role.findMany() + await prisma.user.update({ data: { name: prismaUser.name, email: prismaUser.email, password: prismaUser.password, company_id: prismaUser.company_id, - role_id: prismaUser.role_id, + role_id: + prismaRoles.find((prismaRole) => prismaRole.name === prismaUser.role?.name) + ?.id ?? '', has_first_password_reset: prismaUser.has_first_password_reset, }, where: { id: userId }, diff --git a/apps/server/src/database/prisma/seed.ts b/apps/server/src/database/prisma/seed.ts index c039fb7b..c9808ca9 100644 --- a/apps/server/src/database/prisma/seed.ts +++ b/apps/server/src/database/prisma/seed.ts @@ -143,12 +143,12 @@ export async function seed() { ) const fakeUsers = UsersFaker.fakeMany(10, { - role: RolesFaker.fake({ name: 'employee' }), + role: 'employee', companyId: fakeCompany.id, }) fakeUsers.push( UsersFaker.fake({ - role: RolesFaker.fake({ name: 'admin' }), + role: 'admin', companyId: fakeCompany.id, email: 'stockerteampr@gmail.com', password: await new CryptoProvider().hash('stocker123'), diff --git a/apps/web/src/actions/index.ts b/apps/web/src/actions/index.ts index ba64d9c0..4d5477fb 100644 --- a/apps/web/src/actions/index.ts +++ b/apps/web/src/actions/index.ts @@ -1,4 +1,4 @@ export { getCookieAction } from './get-cookie-action' export { setCookieAction } from './set-cookie-action' export { deleteCookieAction } from './delete-cookie-action' -export { verifyUserRoleAction } from './verify-user-role-action' +export { verifyRolePermissionAction } from './verify-role-permission-action' diff --git a/apps/web/src/actions/verify-role-permission-action.ts b/apps/web/src/actions/verify-role-permission-action.ts new file mode 100644 index 00000000..b19f2ba3 --- /dev/null +++ b/apps/web/src/actions/verify-role-permission-action.ts @@ -0,0 +1,14 @@ +'use server' + +import { NextServerApiClient } from '@/api/next/clients/next-server-api-client' +import { AuthService } from '@/api/services' +import type { RolePermission } from '@stocker/core/types' + +export async function verifyRolePermissionAction(permission: RolePermission) { + const apiClient = await NextServerApiClient({ isCacheEnabled: false }) + const authService = AuthService(apiClient) + const response = await authService.getPermissions() + if (response.isFailure) response.throwError() + + return response.body.includes(permission) +} diff --git a/apps/web/src/actions/verify-user-role-action.ts b/apps/web/src/actions/verify-user-role-action.ts deleted file mode 100644 index 04e0197d..00000000 --- a/apps/web/src/actions/verify-user-role-action.ts +++ /dev/null @@ -1,16 +0,0 @@ -'use server' - -import { COOKIES } from '@/constants' -import type { UserDto } from '@stocker/core/dtos' -import { User } from '@stocker/core/entities' -import type { UserRole } from '@stocker/core/types' -import { jwtDecode } from 'jwt-decode' -import { cookies } from 'next/headers' - -export async function verifyUserRoleAction(role: UserRole) { - const jwt = cookies().get(COOKIES.jwt.key) - if (!jwt?.value) return false - - const user = User.create(jwtDecode(jwt.value)) - return user.hasValidRole(role) -} diff --git a/apps/web/src/app/(private)/dashboard/page.tsx b/apps/web/src/app/(private)/dashboard/page.tsx index 6d6a7bc7..c52fd4b4 100644 --- a/apps/web/src/app/(private)/dashboard/page.tsx +++ b/apps/web/src/app/(private)/dashboard/page.tsx @@ -1,9 +1,9 @@ -import { verifyUserRoleAction } from '@/actions' +import { verifyRolePermissionAction } from '@/actions' import { DashboardPage } from '@/ui/components/pages/dashboard' import { notFound } from 'next/navigation' const Page = async () => { - const isValidRole = await verifyUserRoleAction('manager') + const isValidRole = await verifyRolePermissionAction('reports') if (!isValidRole) { return notFound() } diff --git a/apps/web/src/app/(private)/inventory/movements/page.tsx b/apps/web/src/app/(private)/inventory/movements/page.tsx index 9343c915..a00629d1 100644 --- a/apps/web/src/app/(private)/inventory/movements/page.tsx +++ b/apps/web/src/app/(private)/inventory/movements/page.tsx @@ -1,9 +1,9 @@ -import { verifyUserRoleAction } from '@/actions' +import { verifyRolePermissionAction } from '@/actions' import { InventoryMovementsPage } from '@/ui/components/pages/inventory-movements' import { notFound } from 'next/navigation' const Page = async () => { - const isValidRole = await verifyUserRoleAction('manager') + const isValidRole = await verifyRolePermissionAction('reports') if (!isValidRole) { return notFound() } diff --git a/apps/web/src/app/(private)/records/employees/page.tsx b/apps/web/src/app/(private)/records/employees/page.tsx index e68089e7..c63831f8 100644 --- a/apps/web/src/app/(private)/records/employees/page.tsx +++ b/apps/web/src/app/(private)/records/employees/page.tsx @@ -1,9 +1,9 @@ -import { verifyUserRoleAction } from '@/actions' +import { verifyRolePermissionAction } from '@/actions' import { EmployeesPage } from '@/ui/components/pages/employees' import { notFound } from 'next/navigation' const Page = async () => { - const isValidRole = await verifyUserRoleAction('admin') + const isValidRole = await verifyRolePermissionAction('all') if (!isValidRole) return notFound() return diff --git a/apps/web/src/app/(private)/records/locations/page.tsx b/apps/web/src/app/(private)/records/locations/page.tsx index 331f7ca1..fcc79f64 100644 --- a/apps/web/src/app/(private)/records/locations/page.tsx +++ b/apps/web/src/app/(private)/records/locations/page.tsx @@ -1,9 +1,9 @@ -import { verifyUserRoleAction } from '@/actions' +import { verifyRolePermissionAction } from '@/actions' import { LocationsPage } from '@/ui/components/pages/locations' import { notFound } from 'next/navigation' const Page = async () => { - const isValidRole = await verifyUserRoleAction('manager') + const isValidRole = await verifyRolePermissionAction('categories-control') if (!isValidRole) { return notFound() } diff --git a/apps/web/src/app/(private)/records/products/page.tsx b/apps/web/src/app/(private)/records/products/page.tsx index c6db0921..1329264e 100644 --- a/apps/web/src/app/(private)/records/products/page.tsx +++ b/apps/web/src/app/(private)/records/products/page.tsx @@ -1,6 +1,14 @@ +import { notFound } from 'next/navigation' + +import { verifyRolePermissionAction } from '@/actions' import { ProductsPage } from '@/ui/components/pages/products' -const Page = () => { +const Page = async () => { + const isValidRole = await verifyRolePermissionAction('products-control') + if (!isValidRole) { + return notFound() + } + return } diff --git a/apps/web/src/app/(private)/records/suppliers/page.tsx b/apps/web/src/app/(private)/records/suppliers/page.tsx index 82e8accf..e812bc37 100644 --- a/apps/web/src/app/(private)/records/suppliers/page.tsx +++ b/apps/web/src/app/(private)/records/suppliers/page.tsx @@ -1,9 +1,9 @@ -import { verifyUserRoleAction } from '@/actions' +import { verifyRolePermissionAction } from '@/actions' import { SuppliersPage } from '@/ui/components/pages/suppliers' import { notFound } from 'next/navigation' const Page = async () => { - const isValidRole = await verifyUserRoleAction('manager') + const isValidRole = await verifyRolePermissionAction('suppliers-control') if (!isValidRole) { return notFound() } diff --git a/apps/web/src/constants/cache.ts b/apps/web/src/constants/cache.ts index c14d6c7a..919463d6 100644 --- a/apps/web/src/constants/cache.ts +++ b/apps/web/src/constants/cache.ts @@ -47,6 +47,9 @@ export const CACHE = { company: { key: '/company', }, + permissions: { + key: '/permissions', + }, supplier: { key: '/supplier', }, diff --git a/apps/web/src/ui/components/contexts/auth-context/hooks/use-auth-context-provider.ts b/apps/web/src/ui/components/contexts/auth-context/hooks/use-auth-context-provider.ts index ae6ad76f..39679225 100644 --- a/apps/web/src/ui/components/contexts/auth-context/hooks/use-auth-context-provider.ts +++ b/apps/web/src/ui/components/contexts/auth-context/hooks/use-auth-context-provider.ts @@ -47,13 +47,29 @@ export function useAuthContextProvider({ response.throwError() } - const { data, mutate } = useCache({ + async function fetchPermissions() { + const response = await authService.getPermissions() + + if (response.isSuccess) { + return response.body + } + + response.throwError() + } + + const { data: comapanyDto, mutate } = useCache({ fetcher: fetchCompany, key: CACHE.company.key, isEnabled: Boolean(user), }) - const company = data ? Company.create(data) : null + const company = comapanyDto ? Company.create(comapanyDto) : null + + const { data: permissions } = useCache({ + fetcher: fetchPermissions, + key: CACHE.company.key, + isEnabled: Boolean(user), + }) function getRouteByUserRole(role: RoleName) { switch (role) { @@ -197,6 +213,7 @@ export function useAuthContextProvider({ return { user, company, + permissions, login, logout, subscribe, diff --git a/apps/web/src/ui/components/contexts/auth-context/types/auth-context-value.ts b/apps/web/src/ui/components/contexts/auth-context/types/auth-context-value.ts index b3203c8b..66ea577e 100644 --- a/apps/web/src/ui/components/contexts/auth-context/types/auth-context-value.ts +++ b/apps/web/src/ui/components/contexts/auth-context/types/auth-context-value.ts @@ -1,10 +1,12 @@ import type { CompanyDto, UserDto } from '@stocker/core/dtos' import type { Company, User } from '@stocker/core/entities' +import type { RolePermission } from '@stocker/core/types' export type AuthContextValue = { user: User | null company: Company | null jwt: string | null + permissions: RolePermission[] login: (email: string, password: string) => Promise subscribe: (userDto: UserDto, companyDto: CompanyDto) => Promise logout: () => Promise diff --git a/apps/web/src/ui/components/layouts/dashboard/navbar/index.tsx b/apps/web/src/ui/components/layouts/dashboard/navbar/index.tsx index d4988d3a..d1f86fda 100644 --- a/apps/web/src/ui/components/layouts/dashboard/navbar/index.tsx +++ b/apps/web/src/ui/components/layouts/dashboard/navbar/index.tsx @@ -19,8 +19,7 @@ import { useAuthContext } from '@/ui/components/contexts/auth-context' export const Navbar = () => { const { currentRoute } = useNavigation() - const { user } = useAuthContext() - const userLevel = user?.role + const { permissions } = useAuthContext() return ( { - {userLevel !== 'employee' && ( + {permissions.includes('reports') && ( { - {userLevel !== 'employee' && ( + {permissions.includes('reports') && ( { - {userLevel === 'admin' && ( + {permissions.includes('all') && ( { )} - {userLevel !== 'employee' && ( + {permissions.includes('suppliers-control') && ( { )} - {userLevel !== 'employee' && ( + {permissions.includes('categories-control') && ( { )} - {userLevel !== 'employee' && ( + {permissions.includes('locations-control') && ( { Funcionário - Gerente diff --git a/apps/web/src/ui/components/pages/employees/use-employees-page.ts b/apps/web/src/ui/components/pages/employees/use-employees-page.ts index fcd8bf25..9fa12a75 100644 --- a/apps/web/src/ui/components/pages/employees/use-employees-page.ts +++ b/apps/web/src/ui/components/pages/employees/use-employees-page.ts @@ -1,3 +1,5 @@ +import type { RoleName } from '@stocker/core/types' + import { CACHE } from '@/constants' import { useApi, @@ -6,10 +8,8 @@ import { useUrlParamNumber, useUrlParamString, } from '@/ui/hooks' -import type { UserDto } from '@stocker/core/dtos' import { useState } from 'react' import { useAuthContext } from '../../contexts/auth-context' -import type { UserRole } from '@stocker/core/types' export function useEmployeesPage() { const { showSuccess, showError } = useToast() @@ -36,7 +36,7 @@ export function useEmployeesPage() { page, companyId: companyId, name: nameSearchValue, - role: roleSearchValue as UserRole, + role: roleSearchValue as RoleName, }) if (response.isFailure) { showError(response.errorMessage) diff --git a/apps/web/src/ui/components/pages/product-stock/index.tsx b/apps/web/src/ui/components/pages/product-stock/index.tsx index 2fe8756c..b19907ae 100644 --- a/apps/web/src/ui/components/pages/product-stock/index.tsx +++ b/apps/web/src/ui/components/pages/product-stock/index.tsx @@ -14,6 +14,7 @@ import { BatchesTable } from './batches-table' import { InventoryMovementsTable } from './inventory-movements-table' import { Icon } from '../../commons/icon' import { AlertDialog } from '../../commons/alert-dialog' +import { useAuthContext } from '../../contexts/auth-context' type ProductStockPageProps = { productDto: ProductDto @@ -35,6 +36,7 @@ export const ProductStockPage = ({ productDto }: ProductStockPageProps) => { handleRegisterInboundInventoryMovementFormSubmit, handleRegisterOutboundInventoryMovementFormSubmit, } = useProductStockPage(productDto) + const { permissions } = useAuthContext() return (
@@ -57,45 +59,47 @@ export const ProductStockPage = ({ productDto }: ProductStockPageProps) => {
-
- }> - Lançamento de entrada - - } - > - {(closeDrawer) => ( - { - await handleRegisterInboundInventoryMovementFormSubmit(newBatch) - closeDrawer() - }} - /> - )} - + {permissions.includes('inventory-movements') && ( +
+ }> + Lançamento de entrada + + } + > + {(closeDrawer) => ( + { + await handleRegisterInboundInventoryMovementFormSubmit(newBatch) + closeDrawer() + }} + /> + )} + - }> - Lançamento de saída - - } - > - {(closeDrawer) => ( - { - closeDrawer() - handleRegisterOutboundInventoryMovementFormSubmit(itemsCount) - }} - /> - )} - -
+ }> + Lançamento de saída + + } + > + {(closeDrawer) => ( + { + closeDrawer() + handleRegisterOutboundInventoryMovementFormSubmit(itemsCount) + }} + /> + )} + +
+ )} { const { @@ -29,12 +30,13 @@ export const StocksPage = () => { filterByNameValue, products, totalPages, + stockLevelSearch, handleSearchChange, handlePageChange, - stockLevelSearch, handleStockLevelSearchChange, handleCategorySearchChange, } = useStocksPage() + const { permissions } = useAuthContext() return (
@@ -69,13 +71,15 @@ export const StocksPage = () => {
- - - + {permissions.includes('csv-export') && ( + + + + )}
> logout(): Promise + getPermissions(): Promise> requestPasswordReset(email: string): Promise> updateAccount( userDto: Partial,