Yetki düzenlemesi, Access Denied ve NotFound

This commit is contained in:
Sedat Öztürk 2025-08-12 22:42:32 +03:00
parent 821806a8db
commit e97d560761
13 changed files with 180 additions and 87 deletions

View file

@ -2497,6 +2497,12 @@
"en": "Access Failed Count",
"tr": "Başarısız Erişim Sayısı"
},
{
"resourceName": "Platform",
"key": "Abp.Identity.Ai",
"en": "AI Assistant",
"tr": "Yapay Zeka Asistanı"
},
{
"resourceName": "Platform",
"key": "Abp.Identity.Profile",
@ -6352,7 +6358,7 @@
{
"resourceName": "Platform",
"key": "Public.notFound.button",
"tr": "Ana Sayfaya Dön",
"tr": "Ana Sayfa'ya Dön",
"en": "Return to Homepage"
},
{
@ -10602,6 +10608,14 @@
"IsEnabled": true,
"MultiTenancySide": 3
},
{
"GroupName": "AbpIdentity",
"Name": "Abp.Identity.Ai",
"ParentName": null,
"DisplayName": "Abp.Identity.Ai",
"IsEnabled": true,
"MultiTenancySide": 3
},
{
"GroupName": "AbpIdentity",
"Name": "Abp.Identity.PermissionGroups",
@ -25519,14 +25533,14 @@
"path": "/admin/menuManager",
"componentPath": "@/views/menu/MenuManager",
"routeType": "protected",
"authority": []
"authority": ["App.Menus.Manager"]
},
{
"key": "admin.listFormManagement.wizard",
"path": "/admin/listform/wizard",
"componentPath": "@/views/admin/listForm/Wizard",
"routeType": "protected",
"authority": []
"authority": ["App.Listforms.Wizard"]
},
{
"key": "admin.listFormManagement.edit",
@ -25547,14 +25561,14 @@
"path": "/admin/forumManagement",
"componentPath": "@/views/forum/Management",
"routeType": "protected",
"authority": []
"authority": ["App.ForumManagement"]
},
{
"key": "admin.ai",
"path": "/admin/ai",
"componentPath": "@/views/ai/Assistant",
"routeType": "protected",
"authority": []
"authority": ["Abp.Identity.Ai"]
},
{
"key": "admin.profile.general",
@ -25596,28 +25610,28 @@
"path": "/admin/settings",
"componentPath": "@/views/settings/Settings",
"routeType": "protected",
"authority": []
"authority": ["App.Setting"]
},
{
"key": "admin.identity.user.detail",
"path": "/admin/users/detail/:userId",
"componentPath": "@/views/admin/user-management/Details",
"routeType": "protected",
"authority": []
"authority": ["AbpIdentity.Users.Update"]
},
{
"key": "admin.identity.ous",
"path": "/admin/ous",
"componentPath": "@/views/admin/organization-unit/OrganizationUnits",
"routeType": "protected",
"authority": []
"authority": ["Abp.Identity.OrganizationUnits"]
},
{
"key": "admin.forum",
"path": "/admin/forum",
"componentPath": "@/views/forum/Forum",
"routeType": "protected",
"authority": []
"authority": ["App.ForumManagement.Publish"]
},
{
"key": "admin.list",
@ -25666,70 +25680,70 @@
"path": "/admin/developerkit",
"componentPath": "@/views/developerKit/DashboardPage",
"routeType": "protected",
"authority": []
"authority": ["App.DeveloperKit"]
},
{
"key": "admin.developerkit.entities",
"path": "/admin/developerkit/entities",
"componentPath": "@/views/developerKit/EntityPage",
"routeType": "protected",
"authority": []
"authority": ["App.DeveloperKit.Entity"]
},
{
"key": "admin.developerkit.entities.new",
"path": "/admin/developerkit/entities/new",
"componentPath": "@/views/developerKit/EntityDetailPage",
"routeType": "protected",
"authority": []
"authority": ["App.DeveloperKit.Entity"]
},
{
"key": "admin.developerkit.entities.edit",
"path": "/admin/developerkit/entities/edit/:id",
"componentPath": "@/views/developerKit/EntityDetailPage",
"routeType": "protected",
"authority": []
"authority": ["App.DeveloperKit.Entity"]
},
{
"key": "admin.developerkit.migrations",
"path": "/admin/developerkit/migrations",
"componentPath": "@/views/developerKit/MigrationPage",
"routeType": "protected",
"authority": []
"authority": ["App.DeveloperKit.Migrations"]
},
{
"key": "admin.developerkit.endpoints",
"path": "/admin/developerkit/endpoints",
"componentPath": "@/views/developerKit/EndpointPage",
"routeType": "protected",
"authority": []
"authority": ["App.DeveloperKit.Endpoints"]
},
{
"key": "admin.developerkit.components",
"path": "/admin/developerkit/components",
"componentPath": "@/views/developerKit/ComponentPage",
"routeType": "protected",
"authority": []
"authority": ["App.DeveloperKit.Components"]
},
{
"key": "admin.developerkit.components.new",
"path": "/admin/developerkit/components/new",
"componentPath": "@/views/developerKit/ComponentDetailPage",
"routeType": "protected",
"authority": []
"authority": ["App.DeveloperKit.Components"]
},
{
"key": "admin.developerkit.components.view",
"path": "/admin/developerkit/components/view/:id",
"componentPath": "@/views/developerKit/ComponentDetailPage",
"routeType": "protected",
"authority": []
"authority": ["App.DeveloperKit.Components"]
},
{
"key": "admin.developerkit.components.edit",
"path": "/admin/developerkit/components/edit/:id",
"componentPath": "@/views/developerKit/CodePage",
"routeType": "protected",
"authority": []
"authority": ["App.DeveloperKit.Components" ]
}
],
"CustomEndpoints": [

View file

@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
"revision": "0.0ii4qr2m5co"
"revision": "0.rs1dfl4jqho"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View file

@ -38,58 +38,58 @@ const PublicLayout = lazy(() => import('./PublicLayout'))
const Layout = () => {
const location = useLocation()
const layoutType = useStoreState((state) => state.theme.layout.type) as LayoutType
const layoutType = useStoreState((s) => s.theme.layout.type) as LayoutType
const { routes, loading } = useDynamicRoutes()
const { authenticated } = useAuth()
useDirection() // Her zaman çağrılmalı
useLocale() // Her zaman çağrılmalı
useDirection()
useLocale()
const currentPath = location.pathname
const isAdminPath = currentPath.startsWith('/admin')
const route = useMemo(() => {
if (!routes || routes.length === 0) return undefined
const matched = routes.find((route) => {
if (!route.path.includes(':')) return route.path === currentPath
const regexPath = route.path.replace(/:[^/]+/g, '[^/]+')
const regex = new RegExp(`^${regexPath}$`)
return regex.test(currentPath)
})
return matched
}, [routes, currentPath])
const AppLayout = useMemo(() => {
if (authenticated && route?.routeType === 'protected') {
// 1) Admin path ise, route bulunmasa bile admin layout'u göster
if (isAdminPath) {
return layouts[layoutType]
}
if (route?.routeType === 'authenticated' || hasSubdomain()) {
return AuthLayout
// 2) Admin değil ve route bulunamadı -> PublicLayout
if (!route) {
return PublicLayout
}
// 3) Mevcut kurallar
if (authenticated && route.routeType === 'protected') {
return layouts[layoutType]
}
if (route.routeType === 'authenticated' || hasSubdomain()) {
return AuthLayout
}
return PublicLayout
}, [layoutType, authenticated, route])
}, [isAdminPath, route, layoutType, authenticated])
// ❗ KOŞUL BURADA OLMALI: HOOKLARDAN SONRA
if (loading) {
return (
<div className="flex flex-auto flex-col h-[100vh]">
<Loading loading={true} />
<Loading loading />
</div>
)
}
return (
<Suspense
fallback={
<div className="flex flex-auto flex-col h-[100vh]">
<Loading loading={true} />
</div>
}
>
<Suspense fallback={<div className="flex flex-auto flex-col h-[100vh]"><Loading loading /></div>}>
<AppLayout />
</Suspense>
)

View file

@ -1,6 +1,8 @@
// AuthorityGuard.tsx
import { PropsWithChildren } from 'react'
import { Navigate } from 'react-router-dom'
import { Navigate, useLocation } from 'react-router-dom'
import useAuthority from '@/utils/hooks/useAuthority'
import { getAccessDeniedPath } from '@/utils/routing'
type AuthorityGuardProps = PropsWithChildren<{
userAuthority?: string[]
@ -9,10 +11,15 @@ type AuthorityGuardProps = PropsWithChildren<{
const AuthorityGuard = (props: AuthorityGuardProps) => {
const { userAuthority = [], authority = [], children } = props
const roleMatched = useAuthority(userAuthority, authority)
const location = useLocation()
return <>{roleMatched ? children : <Navigate to="/access-denied" />}</>
if (!roleMatched) {
const to = getAccessDeniedPath(location.pathname)
return <Navigate to={to} replace state={{ from: location }} />
}
return <>{children}</>
}
export default AuthorityGuard

View file

@ -1,18 +1,24 @@
// PermissionGuard.tsx
import { usePermission } from '@/utils/hooks/usePermission'
import { PropsWithChildren } from 'react'
import { Navigate } from 'react-router-dom'
import { Navigate, useLocation } from 'react-router-dom'
import { getAccessDeniedPath } from '@/utils/routing'
type PermissionGuardProps = PropsWithChildren<{
permissions?: string[]
}>
const PermissionGuard = (props: PermissionGuardProps) => {
const { permissions = [], children } = props
const PermissionGuard = ({ permissions = [], children }: PermissionGuardProps) => {
const { checkPermissions } = usePermission()
const permissionsMatched = checkPermissions(permissions)
const location = useLocation()
return <>{permissionsMatched ? children : <Navigate to="/access-denied" />}</>
if (!permissionsMatched) {
const to = getAccessDeniedPath(location.pathname)
return <Navigate to={to} replace state={{ from: location }} />
}
return <>{children}</>
}
export default PermissionGuard

View file

@ -1,6 +1,7 @@
import Tooltip from '@/components/ui/Tooltip'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission'
import { Helmet } from 'react-helmet'
import { FcAssistant } from 'react-icons/fc'
import { useNavigate } from 'react-router-dom'
@ -8,15 +9,17 @@ import { useNavigate } from 'react-router-dom'
const AiAssistant = () => {
const { translate } = useLocalization()
const navigate = useNavigate()
const { checkPermissions } = usePermission()
const canViewAi = checkPermissions(['Abp.Identity.Ai'])
if (!canViewAi) {
return null
}
return (
<>
<Helmet
titleTemplate="%s | Kurs Platform"
title={translate('::' + 'AI Assistant')}
defaultTitle="Kurs Platform"
></Helmet>
<Tooltip title="AI Asistan">
<Tooltip title={translate('::Abp.Identity.Ai')}>
<div
onClick={() => navigate(ROUTES_ENUM.protected.admin.ai)}
className="flex items-center justify-center w-9 h-9 m-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors duration-200"

View file

@ -1,4 +1,5 @@
import React, { useMemo } from 'react'
// DynamicRouter.tsx
import React from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import { mapDynamicRoutes, loadComponent } from './dynamicRouteLoader'
import { useDynamicRoutes } from './dynamicRoutesContext'
@ -8,17 +9,20 @@ import PageContainer from '@/components/template/PageContainer'
import { ROUTES_ENUM } from './route.constant'
import { hasSubdomain } from '@/utils/subdomain'
// AccessDenied ve NotFound'u dinamiklikten çıkarıyoruz
const AccessDenied = React.lazy(() => import('@/views/AccessDenied'))
const NotFound = React.lazy(() => import('@/views/NotFound'))
export const DynamicRouter: React.FC = () => {
const { routes, loading, error } = useDynamicRoutes()
const dynamicRoutes = useMemo(() => mapDynamicRoutes(routes), [routes])
const NotFoundComponent = useMemo(() => loadComponent('views/NotFound'), [])
const dynamicRoutes = React.useMemo(() => mapDynamicRoutes(routes), [routes])
if (loading) return <div>Loading...</div>
if (error) return <div>Hata: {error}</div>
return (
<Routes>
{/* ADMIN */}
<Route path="/admin/*" element={<ProtectedRoute />}>
{dynamicRoutes
.filter((r) => r.routeType === 'protected')
@ -27,7 +31,7 @@ export const DynamicRouter: React.FC = () => {
return (
<Route
key={route.key}
path={route.path.replace(/^\/admin\/?/, '') || '.'} // Remove /admin prefix for nested routes
path={route.path.replace(/^\/admin\/?/, '') || '.'}
element={
<PermissionGuard permissions={route.authority}>
<PageContainer>
@ -40,9 +44,36 @@ export const DynamicRouter: React.FC = () => {
/>
)
})}
{/* admin default */}
<Route index element={<Navigate to={ROUTES_ENUM.protected.dashboard} replace />} />
{/* admin access denied (statik) */}
<Route
path="access-denied"
element={
<PageContainer>
<React.Suspense fallback={<div>Loading...</div>}>
<AccessDenied />
</React.Suspense>
</PageContainer>
}
/>
{/* admin not found (statik) */}
<Route
path="*"
element={
<PageContainer>
<React.Suspense fallback={<div>Loading...</div>}>
<NotFound />
</React.Suspense>
</PageContainer>
}
/>
</Route>
{/* Auth/Public dinamik rotalar */}
{dynamicRoutes
.filter((r) =>
hasSubdomain() ? r.routeType === 'authenticated' : r.routeType !== 'protected',
@ -62,6 +93,7 @@ export const DynamicRouter: React.FC = () => {
)
})}
{/* root redirect */}
<Route
path="/"
element={
@ -72,16 +104,23 @@ export const DynamicRouter: React.FC = () => {
}
/>
{/* public access denied (statik) */}
<Route
path="/access-denied"
element={
<React.Suspense fallback={<div>Loading...</div>}>
<AccessDenied />
</React.Suspense>
}
/>
{/* public not found (statik) */}
<Route
path="*"
element={
hasSubdomain() ? (
<Navigate to={ROUTES_ENUM.authenticated.login} replace />
) : (
<React.Suspense fallback={<div>Loading...</div>}>
<NotFoundComponent />
<NotFound />
</React.Suspense>
)
}
/>
</Routes>

View file

@ -7,6 +7,7 @@ export const ROUTES_ENUM = {
demo: '/demo',
blog: '/blog',
contact: '/contact',
accessDenied: '/access-denied',
},
authenticated: {
login: '/login',
@ -67,5 +68,6 @@ export const ROUTES_ENUM = {
chart: '/admin/chart/:chartCode',
pivot: '/admin/pivot/:listFormCode',
},
accessDenied: '/admin/access-denied',
},
}

5
ui/src/utils/routing.ts Normal file
View file

@ -0,0 +1,5 @@
import { ROUTES_ENUM } from "@/routes/route.constant";
// src/utils/routing.ts
export const getAccessDeniedPath = (pathname: string) =>
pathname.startsWith('/admin') ? ROUTES_ENUM.protected.accessDenied : ROUTES_ENUM.public.accessDenied

View file

@ -10,19 +10,27 @@ const AccessDenied = () => {
const navigate = useNavigate()
const { translate } = useLocalization()
const currentPath = location.pathname
const isAdminPath = currentPath.startsWith('/admin')
return (
<Container className="h-full">
<div className="h-full flex flex-col items-center justify-center p-24">
<div className="h-full flex flex-col items-center justify-center p-28">
<DoubleSidedImage
src="/img/others/img-2.png"
darkModeSrc="/img/others/img-2-dark.png"
alt={ translate('::AccessDenied')}
alt={translate('::AccessDenied')}
/>
<div className="mt-6 text-center">
<h3 className="mb-2">{ translate('::AccessDenied')}</h3>
<p className="text-base">{ translate('::AccessDeniedMessage')}</p>
<h3 className="mb-2">{translate('::AccessDenied')}</h3>
<p className="text-base">{translate('::AccessDeniedMessage')}</p>
</div>
<Button size="xs" className="mt-2" variant="default" onClick={() => navigate(ROUTES_ENUM.protected.dashboard)}>
<Button
size="xs"
className="mt-2"
variant="default"
onClick={() => navigate(isAdminPath ? ROUTES_ENUM.protected.dashboard : ROUTES_ENUM.public.home)}
>
<MdArrowBack />
</Button>
</div>

View file

@ -1,25 +1,30 @@
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useNavigate } from 'react-router-dom'
const NotFoundPage = () => {
const navigate = useNavigate()
const { translate } = useLocalization()
const currentPath = location.pathname
const isAdminPath = currentPath.startsWith('/admin')
return (
<div className="p-20">
<div className="p-28">
<div className="flex items-center justify-center font-inter">
<div className="text-[8rem] sm:text-[10rem] md:text-[12rem] font-bold bg-gradient-to-br from-primary to-secondary bg-clip-text animate-pulse">
404
</div>
</div>
<p className="flex items-center justify-center text-xl mb-6 text-gray-600">
Aradığınız sayfa bulunamadı.
{translate('::Public.notFound.message')}
</p>
<div className="flex items-center justify-center font-inter">
<button
onClick={() => navigate(ROUTES_ENUM.protected.dashboard)}
onClick={() => navigate(isAdminPath ? ROUTES_ENUM.protected.dashboard : ROUTES_ENUM.public.home)}
className="px-6 py-3 bg-primary rounded-xl shadow hover:bg-secondary transition"
>
Ana Sayfa'ya Dön
{translate('::Public.notFound.button')}
</button>
</div>
</div>

View file

@ -10,6 +10,7 @@ import DialogProvider from './shared/DialogContext'
import DialogShowComponent from './shared/DialogContext/DialogShowComponent'
import UiDialog from './shared/UiDialog'
import { DynamicRouter } from '@/routes/dynamicRouter'
import { getAccessDeniedPath } from '@/utils/routing'
interface ViewsProps {
pageContainerType?: 'default' | 'gutterless' | 'contained'
@ -48,7 +49,11 @@ const Views = (props: ViewsProps) => {
</Alert>
)}
{errors?.some((e) => e.statusCode === '403' || e.statusCode === '401') && (
<Navigate to="/access-denied" replace />
<Navigate
to={getAccessDeniedPath(location.pathname)}
replace
state={{ from: location }}
/>
)}
{errors?.map((e) => (
<UiDialog

View file

@ -14,6 +14,7 @@ import { GridColumnData } from '../list/GridColumnData'
import { addCss, addJs } from '../list/Utils'
import { PermissionResults, RowMode, SimpleItemWithColData } from './types'
import { EditingFormItemDto, GridDto, PlatformEditorTypes } from '@/proxy/form/models'
import { getAccessDeniedPath } from '@/utils/routing'
const useGridData = (props: {
mode: RowMode
@ -287,16 +288,14 @@ const useGridData = (props: {
// Auth check
useEffect(() => {
if (!permissionResults) {
return
}
if (!permissionResults) return
if (
(mode === 'new' && !permissionResults.c) ||
(mode === 'edit' && !permissionResults.u) ||
(mode === 'view' && !permissionResults.r)
) {
navigate('/access-denied')
const noCreate = mode === 'new' && !permissionResults.c
const noUpdate = mode === 'edit' && !permissionResults.u
const noRead = mode === 'view' && !permissionResults.r
if (noCreate || noUpdate || noRead) {
navigate(getAccessDeniedPath(location.pathname), { replace: true, state: { from: location } })
}
}, [permissionResults])