From 390bd19a55def8e31ef70c8f9dae01c65725dd27 Mon Sep 17 00:00:00 2001 From: Luc Date: Wed, 18 Dec 2024 23:15:50 +0100 Subject: [PATCH] Update wildcard policy handling, settings, nav, and more --- engine/src/auth/permissions.rs | 9 +- engine/src/routes/policy/mod.rs | 6 +- web/src/api/policy.ts | 14 ++- web/src/api/schema.gen.ts | 2 +- web/src/components/LeafletPreview.tsx | 7 +- web/src/components/Navbar.tsx | 134 +++++++++++---------- web/src/components/logs/AllLogsSection.tsx | 17 ++- web/src/components/settings/nav.tsx | 20 ++- web/src/routeTree.gen.ts | 80 +++++++----- web/src/routes/logs/index.tsx | 20 --- web/src/routes/settings/fields/index.tsx | 41 ++++--- web/src/routes/settings/logs/index.tsx | 22 ++++ web/src/routes/settings/tags.tsx | 16 +++ 13 files changed, 233 insertions(+), 155 deletions(-) delete mode 100644 web/src/routes/logs/index.tsx create mode 100644 web/src/routes/settings/logs/index.tsx create mode 100644 web/src/routes/settings/tags.tsx diff --git a/engine/src/auth/permissions.rs b/engine/src/auth/permissions.rs index 173f9e4..23475dd 100644 --- a/engine/src/auth/permissions.rs +++ b/engine/src/auth/permissions.rs @@ -104,7 +104,7 @@ impl User { // None // } - let permissions = Self::enumerate_permissions(db, user, resource_type, &resource_id).await; + let permissions = Self::enumerate_permissions(db, user, resource_type, resource_id).await; let bv = match actions { Actions::One(action) => permissions.contains(&action), @@ -166,11 +166,12 @@ impl User { db: &Database, user: impl Into>, resource_type: &str, - resource_id: &Option<&str>, + resource_id: impl Into>, ) -> Vec { let user: Option = user.into(); let user_id = user.unwrap_or(-1).to_string(); let is_authed = user.is_some().to_string(); + let resource_id = resource_id.into(); debug!( "Enumerating permissions for user: {:?} on resource: {:?} / {:?}", @@ -179,6 +180,8 @@ impl User { resource_id ); + let resource_id = resource_id.as_ref(); + let policies = query!( r#"SELECT action FROM policies WHERE resource_type = $1 AND @@ -187,7 +190,7 @@ impl User { (subject_type = 'authed' AND subject_id = $4)) "#, resource_type, - resource_id.unwrap_or(""), + resource_id, user_id, is_authed, ) diff --git a/engine/src/routes/policy/mod.rs b/engine/src/routes/policy/mod.rs index 2c7fa5a..f4c90ce 100644 --- a/engine/src/routes/policy/mod.rs +++ b/engine/src/routes/policy/mod.rs @@ -24,14 +24,16 @@ impl PolicyApi { /// Example: "item" | "product" | "media" | "user" resource_type: Query, /// Example: "1234" | "AB123" - resource_id: Query, + resource_id: Query>, ) -> Result>> { + let resource_id = resource_id.as_ref().map(|id| id.as_str()); + Ok(Json( User::enumerate_permissions( &state.database, user, &resource_type.0, - &Some(resource_id.0.to_string().as_str()), + resource_id, ) .await, )) diff --git a/web/src/api/policy.ts b/web/src/api/policy.ts index 21eed68..c3e72aa 100644 --- a/web/src/api/policy.ts +++ b/web/src/api/policy.ts @@ -2,15 +2,19 @@ import { queryOptions, useQuery } from '@tanstack/react-query'; import { ApiRequest, apiRequest } from './core'; -export const getPolicy = (resourceType: string, resourceId: string) => +export const getPolicy = (resourceType: string, resourceId?: string) => queryOptions({ queryKey: ['policy', resourceType, resourceId], queryFn: async () => { const response = await apiRequest('/policy/enumerate', 'get', { - query: { - resource_type: resourceType, - resource_id: resourceId, - }, + query: resourceId + ? { + resource_type: resourceType, + resource_id: resourceId, + } + : { + resource_type: resourceType, + }, }); return response.data; diff --git a/web/src/api/schema.gen.ts b/web/src/api/schema.gen.ts index 647d930..fb32993 100644 --- a/web/src/api/schema.gen.ts +++ b/web/src/api/schema.gen.ts @@ -1491,7 +1491,7 @@ export type paths = { /** @description Example: "item" | "product" | "media" | "user" */ resource_type: string; /** @description Example: "1234" | "AB123" */ - resource_id: string; + resource_id?: string; }; header?: never; path?: never; diff --git a/web/src/components/LeafletPreview.tsx b/web/src/components/LeafletPreview.tsx index 764e3d8..404a484 100644 --- a/web/src/components/LeafletPreview.tsx +++ b/web/src/components/LeafletPreview.tsx @@ -31,9 +31,10 @@ export const LeafletPreview: FC<{ latitude: number; longitude: number }> = ({ // style={{ height: 80, width: 80 }} > ); diff --git a/web/src/components/Navbar.tsx b/web/src/components/Navbar.tsx index 2393a48..736567c 100644 --- a/web/src/components/Navbar.tsx +++ b/web/src/components/Navbar.tsx @@ -19,6 +19,7 @@ import { import { useAuth } from '@/api/auth'; import { BASE_URL } from '@/api/core'; import { useMe } from '@/api/me'; +import { useHasPolicy } from '@/api/policy'; import * as DropdownMenu from '@/components/ui/Dropdown'; import { Button } from './ui/Button'; @@ -26,75 +27,76 @@ import { AvatarHolder, getInitials } from './UserProfile'; const LOGIN_URL = BASE_URL + 'login'; -const navLinks = [ - { - path: '/items', - name: 'Items', - icon: , - slug: 'items-navlink', - }, - { - path: '/products', - name: 'Products', - icon: , - slug: 'products-navlink', - }, - { - path: '/search', - name: 'Search', - icon: , - slug: 'search-navlink', - }, - { - path: '/create', - name: 'Create', - icon: , - slug: 'create-navlink', - }, -] as const; - -const navLinks2 = [ - { - path: '/tags/', - name: 'Tags', - icon: , - slug: 'tags-navlink', - }, - { - path: '/settings/fields', - name: 'Fields', - icon: , - slug: 'fields-navlink', - }, - { - path: '/media/', - name: 'Media', - icon: , - slug: 'media-navlink', - }, - { - path: '/logs/', - name: 'Logs', - icon: , - slug: 'logs-navlink', - }, - { - path: '/users/', - name: 'Users', - icon: , - slug: 'users-navlink', - }, - { - path: '/settings', - name: 'Settings', - icon: , - slug: 'settings-navlink', - }, -] as const; - export const Navbar = () => { const { token, clearAuthToken } = useAuth(); const { data: meData } = useMe(); + const { ok: hasUsersPermissions } = useHasPolicy('user', '', 'write'); + + const navLinks = [ + { + path: '/items', + name: 'Items', + icon: , + slug: 'items-navlink', + }, + { + path: '/products', + name: 'Products', + icon: , + slug: 'products-navlink', + }, + { + path: '/search', + name: 'Search', + icon: , + slug: 'search-navlink', + }, + { + path: '/create', + name: 'Create', + icon: , + slug: 'create-navlink', + }, + ] as const; + + const navLinks2 = [ + { + path: '/settings/tags', + name: 'Tags', + icon: , + slug: 'tags-navlink', + }, + { + path: '/settings/fields', + name: 'Fields', + icon: , + slug: 'fields-navlink', + }, + { + path: '/media/', + name: 'Media', + icon: , + slug: 'media-navlink', + }, + { + path: '/settings/logs', + name: 'Logs', + icon: , + slug: 'logs-navlink', + }, + hasUsersPermissions && { + path: '/settings/users', + name: 'Users', + icon: , + slug: 'users-navlink', + }, + { + path: '/settings', + name: 'Settings', + icon: , + slug: 'settings-navlink', + }, + ].filter(Boolean); const login_here_url = LOGIN_URL + '?redirect=' + encodeURIComponent(window.location.href); diff --git a/web/src/components/logs/AllLogsSection.tsx b/web/src/components/logs/AllLogsSection.tsx index 581604e..7a60c2b 100644 --- a/web/src/components/logs/AllLogsSection.tsx +++ b/web/src/components/logs/AllLogsSection.tsx @@ -6,15 +6,12 @@ export const AllLogsSection = () => { const { data: logs } = useAllLogs(); return ( -
-

Logs

-
-
    - {logs?.map((log) => ( - - ))} -
-
-
+
    + {logs?.map((log) => ( +
    + +
    + ))} +
); }; diff --git a/web/src/components/settings/nav.tsx b/web/src/components/settings/nav.tsx index 879621d..8682475 100644 --- a/web/src/components/settings/nav.tsx +++ b/web/src/components/settings/nav.tsx @@ -2,12 +2,14 @@ import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; import { LuBrain, + LuClipboardType, LuClock, LuHardDrive, LuKey, LuScroll, LuSearch, LuSettings, + LuTag, } from 'react-icons/lu'; export const SettingsNav = () => { @@ -17,7 +19,18 @@ export const SettingsNav = () => { [ ['', [['/settings', 'General', ]]], [ - 'Instance Settings', + 'Organizational', + [ + ['/settings/tags', 'Tags', ], + [ + '/settings/fields', + 'Field Definitions', + , + ], + ], + ], + [ + 'Instance', [ ['/settings/search', 'Search', ], [ @@ -41,7 +54,10 @@ export const SettingsNav = () => { ], [ 'System', - [['/settings/build', 'Software Info', ]], + [ + ['/settings/logs', 'Access Logs', ], + ['/settings/build', 'Software Info', ], + ], ], ] as const ).map(([group, items]) => ( diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index ab94d26..31e6b01 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -19,15 +19,16 @@ import { Route as IndexImport } from './routes/index' import { Route as SettingsIndexImport } from './routes/settings/index' import { Route as SearchIndexImport } from './routes/search/index' import { Route as ProductsIndexImport } from './routes/products/index' -import { Route as LogsIndexImport } from './routes/logs/index' import { Route as ItemsIndexImport } from './routes/items/index' import { Route as UserUserIdImport } from './routes/user/$userId' +import { Route as SettingsTagsImport } from './routes/settings/tags' import { Route as SettingsStorageImport } from './routes/settings/storage' import { Route as SettingsSessionsImport } from './routes/settings/sessions' import { Route as SettingsSearchImport } from './routes/settings/search' import { Route as SettingsPatImport } from './routes/settings/pat' import { Route as SettingsIntelligenceImport } from './routes/settings/intelligence' import { Route as SettingsBuildImport } from './routes/settings/build' +import { Route as SettingsLogsIndexImport } from './routes/settings/logs/index' import { Route as SettingsFieldsIndexImport } from './routes/settings/fields/index' import { Route as ItemItemIdIndexImport } from './routes/item/$itemId/index' import { Route as ItemItemIdEditImport } from './routes/item/$itemId/edit' @@ -82,12 +83,6 @@ const ProductsIndexRoute = ProductsIndexImport.update({ getParentRoute: () => rootRoute, } as any) -const LogsIndexRoute = LogsIndexImport.update({ - id: '/logs/', - path: '/logs/', - getParentRoute: () => rootRoute, -} as any) - const ItemsIndexRoute = ItemsIndexImport.update({ id: '/items/', path: '/items/', @@ -100,6 +95,12 @@ const UserUserIdRoute = UserUserIdImport.update({ getParentRoute: () => rootRoute, } as any) +const SettingsTagsRoute = SettingsTagsImport.update({ + id: '/settings/tags', + path: '/settings/tags', + getParentRoute: () => rootRoute, +} as any) + const SettingsStorageRoute = SettingsStorageImport.update({ id: '/settings/storage', path: '/settings/storage', @@ -136,6 +137,12 @@ const SettingsBuildRoute = SettingsBuildImport.update({ getParentRoute: () => rootRoute, } as any) +const SettingsLogsIndexRoute = SettingsLogsIndexImport.update({ + id: '/settings/logs/', + path: '/settings/logs/', + getParentRoute: () => rootRoute, +} as any) + const SettingsFieldsIndexRoute = SettingsFieldsIndexImport.update({ id: '/settings/fields/', path: '/settings/fields/', @@ -235,6 +242,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsStorageImport parentRoute: typeof rootRoute } + '/settings/tags': { + id: '/settings/tags' + path: '/settings/tags' + fullPath: '/settings/tags' + preLoaderRoute: typeof SettingsTagsImport + parentRoute: typeof rootRoute + } '/user/$userId': { id: '/user/$userId' path: '/user/$userId' @@ -249,13 +263,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ItemsIndexImport parentRoute: typeof rootRoute } - '/logs/': { - id: '/logs/' - path: '/logs' - fullPath: '/logs' - preLoaderRoute: typeof LogsIndexImport - parentRoute: typeof rootRoute - } '/products/': { id: '/products/' path: '/products' @@ -298,6 +305,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsFieldsIndexImport parentRoute: typeof rootRoute } + '/settings/logs/': { + id: '/settings/logs/' + path: '/settings/logs' + fullPath: '/settings/logs' + preLoaderRoute: typeof SettingsLogsIndexImport + parentRoute: typeof rootRoute + } } } @@ -315,15 +329,16 @@ export interface FileRoutesByFullPath { '/settings/search': typeof SettingsSearchRoute '/settings/sessions': typeof SettingsSessionsRoute '/settings/storage': typeof SettingsStorageRoute + '/settings/tags': typeof SettingsTagsRoute '/user/$userId': typeof UserUserIdRoute '/items': typeof ItemsIndexRoute - '/logs': typeof LogsIndexRoute '/products': typeof ProductsIndexRoute '/search': typeof SearchIndexRoute '/settings': typeof SettingsIndexRoute '/item/$itemId/edit': typeof ItemItemIdEditRoute '/item/$itemId': typeof ItemItemIdIndexRoute '/settings/fields': typeof SettingsFieldsIndexRoute + '/settings/logs': typeof SettingsLogsIndexRoute } export interface FileRoutesByTo { @@ -338,15 +353,16 @@ export interface FileRoutesByTo { '/settings/search': typeof SettingsSearchRoute '/settings/sessions': typeof SettingsSessionsRoute '/settings/storage': typeof SettingsStorageRoute + '/settings/tags': typeof SettingsTagsRoute '/user/$userId': typeof UserUserIdRoute '/items': typeof ItemsIndexRoute - '/logs': typeof LogsIndexRoute '/products': typeof ProductsIndexRoute '/search': typeof SearchIndexRoute '/settings': typeof SettingsIndexRoute '/item/$itemId/edit': typeof ItemItemIdEditRoute '/item/$itemId': typeof ItemItemIdIndexRoute '/settings/fields': typeof SettingsFieldsIndexRoute + '/settings/logs': typeof SettingsLogsIndexRoute } export interface FileRoutesById { @@ -362,15 +378,16 @@ export interface FileRoutesById { '/settings/search': typeof SettingsSearchRoute '/settings/sessions': typeof SettingsSessionsRoute '/settings/storage': typeof SettingsStorageRoute + '/settings/tags': typeof SettingsTagsRoute '/user/$userId': typeof UserUserIdRoute '/items/': typeof ItemsIndexRoute - '/logs/': typeof LogsIndexRoute '/products/': typeof ProductsIndexRoute '/search/': typeof SearchIndexRoute '/settings/': typeof SettingsIndexRoute '/item/$itemId/edit': typeof ItemItemIdEditRoute '/item/$itemId/': typeof ItemItemIdIndexRoute '/settings/fields/': typeof SettingsFieldsIndexRoute + '/settings/logs/': typeof SettingsLogsIndexRoute } export interface FileRouteTypes { @@ -387,15 +404,16 @@ export interface FileRouteTypes { | '/settings/search' | '/settings/sessions' | '/settings/storage' + | '/settings/tags' | '/user/$userId' | '/items' - | '/logs' | '/products' | '/search' | '/settings' | '/item/$itemId/edit' | '/item/$itemId' | '/settings/fields' + | '/settings/logs' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -409,15 +427,16 @@ export interface FileRouteTypes { | '/settings/search' | '/settings/sessions' | '/settings/storage' + | '/settings/tags' | '/user/$userId' | '/items' - | '/logs' | '/products' | '/search' | '/settings' | '/item/$itemId/edit' | '/item/$itemId' | '/settings/fields' + | '/settings/logs' id: | '__root__' | '/' @@ -431,15 +450,16 @@ export interface FileRouteTypes { | '/settings/search' | '/settings/sessions' | '/settings/storage' + | '/settings/tags' | '/user/$userId' | '/items/' - | '/logs/' | '/products/' | '/search/' | '/settings/' | '/item/$itemId/edit' | '/item/$itemId/' | '/settings/fields/' + | '/settings/logs/' fileRoutesById: FileRoutesById } @@ -455,15 +475,16 @@ export interface RootRouteChildren { SettingsSearchRoute: typeof SettingsSearchRoute SettingsSessionsRoute: typeof SettingsSessionsRoute SettingsStorageRoute: typeof SettingsStorageRoute + SettingsTagsRoute: typeof SettingsTagsRoute UserUserIdRoute: typeof UserUserIdRoute ItemsIndexRoute: typeof ItemsIndexRoute - LogsIndexRoute: typeof LogsIndexRoute ProductsIndexRoute: typeof ProductsIndexRoute SearchIndexRoute: typeof SearchIndexRoute SettingsIndexRoute: typeof SettingsIndexRoute ItemItemIdEditRoute: typeof ItemItemIdEditRoute ItemItemIdIndexRoute: typeof ItemItemIdIndexRoute SettingsFieldsIndexRoute: typeof SettingsFieldsIndexRoute + SettingsLogsIndexRoute: typeof SettingsLogsIndexRoute } const rootRouteChildren: RootRouteChildren = { @@ -478,15 +499,16 @@ const rootRouteChildren: RootRouteChildren = { SettingsSearchRoute: SettingsSearchRoute, SettingsSessionsRoute: SettingsSessionsRoute, SettingsStorageRoute: SettingsStorageRoute, + SettingsTagsRoute: SettingsTagsRoute, UserUserIdRoute: UserUserIdRoute, ItemsIndexRoute: ItemsIndexRoute, - LogsIndexRoute: LogsIndexRoute, ProductsIndexRoute: ProductsIndexRoute, SearchIndexRoute: SearchIndexRoute, SettingsIndexRoute: SettingsIndexRoute, ItemItemIdEditRoute: ItemItemIdEditRoute, ItemItemIdIndexRoute: ItemItemIdIndexRoute, SettingsFieldsIndexRoute: SettingsFieldsIndexRoute, + SettingsLogsIndexRoute: SettingsLogsIndexRoute, } export const routeTree = rootRoute @@ -510,15 +532,16 @@ export const routeTree = rootRoute "/settings/search", "/settings/sessions", "/settings/storage", + "/settings/tags", "/user/$userId", "/items/", - "/logs/", "/products/", "/search/", "/settings/", "/item/$itemId/edit", "/item/$itemId/", - "/settings/fields/" + "/settings/fields/", + "/settings/logs/" ] }, "/": { @@ -554,15 +577,15 @@ export const routeTree = rootRoute "/settings/storage": { "filePath": "settings/storage.tsx" }, + "/settings/tags": { + "filePath": "settings/tags.tsx" + }, "/user/$userId": { "filePath": "user/$userId.tsx" }, "/items/": { "filePath": "items/index.tsx" }, - "/logs/": { - "filePath": "logs/index.tsx" - }, "/products/": { "filePath": "products/index.tsx" }, @@ -580,6 +603,9 @@ export const routeTree = rootRoute }, "/settings/fields/": { "filePath": "settings/fields/index.tsx" + }, + "/settings/logs/": { + "filePath": "settings/logs/index.tsx" } } } diff --git a/web/src/routes/logs/index.tsx b/web/src/routes/logs/index.tsx deleted file mode 100644 index f199588..0000000 --- a/web/src/routes/logs/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' - -import { AllLogsSection } from '@/components/logs/AllLogsSection' -import { SCPage } from '@/layouts/SimpleCenterPage' - -export const Route = createFileRoute('/logs/')({ - component: () => { - return ( - -
-

- Logs are emitted anytime an item is created, edited, or updated. - This pages provides a full overview of all logs on this instance. -

-
- -
- ) - }, -}) diff --git a/web/src/routes/settings/fields/index.tsx b/web/src/routes/settings/fields/index.tsx index 95f3ea2..d527759 100644 --- a/web/src/routes/settings/fields/index.tsx +++ b/web/src/routes/settings/fields/index.tsx @@ -15,9 +15,10 @@ import { DynamicIcon } from '@/components/DynamicIcon'; import { FieldSelect } from '@/components/form/Select'; import { BaseInput } from '@/components/input/BaseInput'; import { IconInput } from '@/components/input/IconInput'; +import { SettingsNav } from '@/components/settings/nav'; import { Button } from '@/components/ui/Button'; import * as Dialog from '@/components/ui/Dialog'; -import { SCPage } from '@/layouts/SimpleCenterPage'; +import { SidePage } from '@/layouts/SidebarPage'; import { queryClient } from '@/util/query'; const FieldDefinitionEditor = ({ @@ -231,7 +232,28 @@ const RouteComponent = () => { const [creatingField, setCreatingField] = useState(false); return ( - + } + suffix={ + + + + + + Create Field Definition + { + setCreatingField(false); + }} + /> + + + } + >
{data.map((field) => (
{
))}
- - - - - - Create Field Definition - { - setCreatingField(false); - }} - /> - - { /> -
+ ); }; diff --git a/web/src/routes/settings/logs/index.tsx b/web/src/routes/settings/logs/index.tsx new file mode 100644 index 0000000..faf1ab2 --- /dev/null +++ b/web/src/routes/settings/logs/index.tsx @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/react-router'; + +import { AllLogsSection } from '@/components/logs/AllLogsSection'; +import { SettingsNav } from '@/components/settings/nav'; +import { SidePage } from '@/layouts/SidebarPage'; + +export const Route = createFileRoute('/settings/logs/')({ + component: () => { + return ( + }> +
+

+ Logs are emitted anytime an item is created, edited, or + updated. This pages provides a full overview of all logs + on this instance. +

+
+ +
+ ); + }, +}); diff --git a/web/src/routes/settings/tags.tsx b/web/src/routes/settings/tags.tsx new file mode 100644 index 0000000..8f32bd6 --- /dev/null +++ b/web/src/routes/settings/tags.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router'; + +import { SettingsNav } from '@/components/settings/nav'; +import { SidePage } from '@/layouts/SidebarPage'; + +export const Route = createFileRoute('/settings/tags')({ + component: RouteComponent, +}); + +function RouteComponent() { + return ( + }> +
Hello "/settings/tags"!
+
+ ); +}