diff --git a/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json b/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json index f3d01b68..abe70758 100644 --- a/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json +++ b/api/src/Kurs.Platform.DbMigrator/Seeds/SeederData.json @@ -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": [ diff --git a/ui/dev-dist/sw.js b/ui/dev-dist/sw.js index d9e48480..88827ea8 100644 --- a/ui/dev-dist/sw.js +++ b/ui/dev-dist/sw.js @@ -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"), { diff --git a/ui/src/components/layouts/Layouts.tsx b/ui/src/components/layouts/Layouts.tsx index 054fde96..1771f0bc 100644 --- a/ui/src/components/layouts/Layouts.tsx +++ b/ui/src/components/layouts/Layouts.tsx @@ -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: HOOK’LARDAN SONRA if (loading) { return (
- +
) } return ( - - - - } - > + }> ) diff --git a/ui/src/components/route/AuthorityGuard.tsx b/ui/src/components/route/AuthorityGuard.tsx index 3d18a353..983ebb51 100644 --- a/ui/src/components/route/AuthorityGuard.tsx +++ b/ui/src/components/route/AuthorityGuard.tsx @@ -1,18 +1,25 @@ +// 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[] - authority?: string[] + userAuthority?: string[] + authority?: string[] }> const AuthorityGuard = (props: AuthorityGuardProps) => { - const { userAuthority = [], authority = [], children } = props + const { userAuthority = [], authority = [], children } = props + const roleMatched = useAuthority(userAuthority, authority) + const location = useLocation() - const roleMatched = useAuthority(userAuthority, authority) + if (!roleMatched) { + const to = getAccessDeniedPath(location.pathname) + return + } - return <>{roleMatched ? children : } + return <>{children} } export default AuthorityGuard diff --git a/ui/src/components/route/PermissionGuard.tsx b/ui/src/components/route/PermissionGuard.tsx index 5faba93d..88d96cd8 100644 --- a/ui/src/components/route/PermissionGuard.tsx +++ b/ui/src/components/route/PermissionGuard.tsx @@ -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 : } + if (!permissionsMatched) { + const to = getAccessDeniedPath(location.pathname) + return + } + + return <>{children} } export default PermissionGuard diff --git a/ui/src/components/template/AiAssistant.tsx b/ui/src/components/template/AiAssistant.tsx index 5eaba1e7..02af8f58 100644 --- a/ui/src/components/template/AiAssistant.tsx +++ b/ui/src/components/template/AiAssistant.tsx @@ -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 ( <> - - +
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" diff --git a/ui/src/routes/dynamicRouter.tsx b/ui/src/routes/dynamicRouter.tsx index 8cb2eca7..66a87d87 100644 --- a/ui/src/routes/dynamicRouter.tsx +++ b/ui/src/routes/dynamicRouter.tsx @@ -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
Loading...
if (error) return
Hata: {error}
return ( + {/* ADMIN */} }> {dynamicRoutes .filter((r) => r.routeType === 'protected') @@ -27,7 +31,7 @@ export const DynamicRouter: React.FC = () => { return ( @@ -40,9 +44,36 @@ export const DynamicRouter: React.FC = () => { /> ) })} + + {/* admin default */} } /> + + {/* admin access denied (statik) */} + + Loading...
}> + + + + } + /> + + {/* admin not found (statik) */} + + Loading...}> + + + + } + /> + {/* 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 */} { } /> + {/* public access denied (statik) */} + Loading...}> + + + } + /> + + {/* public not found (statik) */} - ) : ( - Loading...}> - - - ) + Loading...}> + + } /> diff --git a/ui/src/routes/route.constant.ts b/ui/src/routes/route.constant.ts index 7afef0d6..ce5fb0f6 100644 --- a/ui/src/routes/route.constant.ts +++ b/ui/src/routes/route.constant.ts @@ -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', }, } diff --git a/ui/src/utils/routing.ts b/ui/src/utils/routing.ts new file mode 100644 index 00000000..1985c349 --- /dev/null +++ b/ui/src/utils/routing.ts @@ -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 diff --git a/ui/src/views/AccessDenied.tsx b/ui/src/views/AccessDenied.tsx index ef048744..f555eb30 100644 --- a/ui/src/views/AccessDenied.tsx +++ b/ui/src/views/AccessDenied.tsx @@ -10,19 +10,27 @@ const AccessDenied = () => { const navigate = useNavigate() const { translate } = useLocalization() + const currentPath = location.pathname + const isAdminPath = currentPath.startsWith('/admin') + return ( -
+
-

{ translate('::AccessDenied')}

-

{ translate('::AccessDeniedMessage')}

+

{translate('::AccessDenied')}

+

{translate('::AccessDeniedMessage')}

-
diff --git a/ui/src/views/NotFound.tsx b/ui/src/views/NotFound.tsx index a121b692..c0853557 100644 --- a/ui/src/views/NotFound.tsx +++ b/ui/src/views/NotFound.tsx @@ -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 ( -
+
404

- Aradığınız sayfa bulunamadı. + {translate('::Public.notFound.message')}

diff --git a/ui/src/views/Views.tsx b/ui/src/views/Views.tsx index 825dd776..5c5b0450 100644 --- a/ui/src/views/Views.tsx +++ b/ui/src/views/Views.tsx @@ -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) => { )} {errors?.some((e) => e.statusCode === '403' || e.statusCode === '401') && ( - + )} {errors?.map((e) => ( { - 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])