Skip to content

Commit 5ded1b3

Browse files
lamppuEtsija
authored andcommitted
fix(ui): Use guard components to protect routes
Fix an issue with the UI redirecting to the 403 page before permissions were loaded by wrapping the corresponding routes with guard components that have proper handling for the loading state. Resolves #4509. Signed-off-by: Johanna Lamppu <johanna.lamppu@doubleopen.org>
1 parent 7c4d7b9 commit 5ded1b3

10 files changed

Lines changed: 157 additions & 92 deletions

File tree

ui/src/routes/admin/route.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* License-Filename: LICENSE
1818
*/
1919

20-
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
20+
import { createFileRoute, Outlet } from '@tanstack/react-router';
2121
import {
2222
Blocks,
2323
Eye,
@@ -28,6 +28,7 @@ import {
2828
User,
2929
} from 'lucide-react';
3030

31+
import { RequireSuperuser } from '@/components/authorization';
3132
import { PageLayout } from '@/components/page-layout';
3233
import { SidebarNavProps } from '@/components/sidebar';
3334

@@ -97,19 +98,14 @@ const Layout = () => {
9798
},
9899
];
99100
return (
100-
<PageLayout sections={sections}>
101-
<Outlet />
102-
</PageLayout>
101+
<RequireSuperuser>
102+
<PageLayout sections={sections}>
103+
<Outlet />
104+
</PageLayout>
105+
</RequireSuperuser>
103106
);
104107
};
105108

106109
export const Route = createFileRoute('/admin')({
107110
component: Layout,
108-
beforeLoad: ({ context }) => {
109-
if (!context.auth.isSuperuser) {
110-
throw redirect({
111-
to: '/403',
112-
});
113-
}
114-
},
115111
});

ui/src/routes/organizations/$orgId/infrastructure-services/route.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,25 @@
1717
* License-Filename: LICENSE
1818
*/
1919

20-
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
20+
import { createFileRoute, Outlet } from '@tanstack/react-router';
21+
22+
import { RequireOrganizationPermission } from '@/components/authorization';
23+
24+
function OrganizationInfrastructureServicesGuard() {
25+
const { orgId } = Route.useParams();
26+
27+
return (
28+
<RequireOrganizationPermission
29+
organizationId={Number.parseInt(orgId)}
30+
permission='WRITE'
31+
>
32+
<Outlet />
33+
</RequireOrganizationPermission>
34+
);
35+
}
2136

2237
export const Route = createFileRoute(
2338
'/organizations/$orgId/infrastructure-services'
2439
)({
25-
component: () => <Outlet />,
26-
beforeLoad: ({ context }) => {
27-
if (!context.permissions.organization?.includes('WRITE')) {
28-
throw redirect({
29-
to: '/403',
30-
});
31-
}
32-
},
40+
component: OrganizationInfrastructureServicesGuard,
3341
});

ui/src/routes/organizations/$orgId/products/$productId/infrastructure-services/route.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,25 @@
1717
* License-Filename: LICENSE
1818
*/
1919

20-
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
20+
import { createFileRoute, Outlet } from '@tanstack/react-router';
21+
22+
import { RequireProductPermission } from '@/components/authorization';
23+
24+
function ProductInfrastructureServicesGuard() {
25+
const { productId } = Route.useParams();
26+
27+
return (
28+
<RequireProductPermission
29+
productId={Number.parseInt(productId)}
30+
permission='WRITE'
31+
>
32+
<Outlet />
33+
</RequireProductPermission>
34+
);
35+
}
2136

2237
export const Route = createFileRoute(
2338
'/organizations/$orgId/products/$productId/infrastructure-services'
2439
)({
25-
component: () => <Outlet />,
26-
beforeLoad: ({ context }) => {
27-
if (!context.permissions.product?.includes('WRITE')) {
28-
throw redirect({
29-
to: '/403',
30-
});
31-
}
32-
},
40+
component: ProductInfrastructureServicesGuard,
3341
});

ui/src/routes/organizations/$orgId/products/$productId/repositories/$repoId/_repo-layout/infrastructure-services/route.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,25 @@
1717
* License-Filename: LICENSE
1818
*/
1919

20-
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
20+
import { createFileRoute, Outlet } from '@tanstack/react-router';
21+
22+
import { RequireRepositoryPermission } from '@/components/authorization';
23+
24+
function RepositoryInfrastructureServicesGuard() {
25+
const { repoId } = Route.useParams();
26+
27+
return (
28+
<RequireRepositoryPermission
29+
repositoryId={Number.parseInt(repoId)}
30+
permission='WRITE'
31+
>
32+
<Outlet />
33+
</RequireRepositoryPermission>
34+
);
35+
}
2136

2237
export const Route = createFileRoute(
2338
'/organizations/$orgId/products/$productId/repositories/$repoId/_repo-layout/infrastructure-services'
2439
)({
25-
component: () => <Outlet />,
26-
beforeLoad: ({ context }) => {
27-
if (!context.permissions.repository?.includes('WRITE')) {
28-
throw redirect({
29-
to: '/403',
30-
});
31-
}
32-
},
40+
component: RepositoryInfrastructureServicesGuard,
3341
});

ui/src/routes/organizations/$orgId/products/$productId/repositories/$repoId/_repo-layout/secrets/route.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,25 @@
1717
* License-Filename: LICENSE
1818
*/
1919

20-
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
20+
import { createFileRoute, Outlet } from '@tanstack/react-router';
21+
22+
import { RequireRepositoryPermission } from '@/components/authorization';
23+
24+
function RepositorySecretsGuard() {
25+
const { repoId } = Route.useParams();
26+
27+
return (
28+
<RequireRepositoryPermission
29+
repositoryId={Number.parseInt(repoId)}
30+
permission='WRITE_SECRETS'
31+
>
32+
<Outlet />
33+
</RequireRepositoryPermission>
34+
);
35+
}
2136

2237
export const Route = createFileRoute(
2338
'/organizations/$orgId/products/$productId/repositories/$repoId/_repo-layout/secrets'
2439
)({
25-
component: () => <Outlet />,
26-
beforeLoad: ({ context }) => {
27-
if (!context.permissions.repository?.includes('WRITE_SECRETS')) {
28-
throw redirect({
29-
to: '/403',
30-
});
31-
}
32-
},
40+
component: RepositorySecretsGuard,
3341
});

ui/src/routes/organizations/$orgId/products/$productId/repositories/$repoId/_repo-layout/users/route.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,29 @@
1717
* License-Filename: LICENSE
1818
*/
1919

20-
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
20+
import { createFileRoute, Outlet } from '@tanstack/react-router';
2121

2222
import { getRepositoryUsersOptions } from '@/api/@tanstack/react-query.gen';
23+
import { RequireRepositoryPermission } from '@/components/authorization';
2324
import { paginationSearchParameterSchema } from '@/schemas';
2425

26+
function RepositoryUsersGuard() {
27+
const { repoId } = Route.useParams();
28+
29+
return (
30+
<RequireRepositoryPermission
31+
repositoryId={Number.parseInt(repoId)}
32+
permission='MANAGE_GROUPS'
33+
>
34+
<Outlet />
35+
</RequireRepositoryPermission>
36+
);
37+
}
38+
2539
export const Route = createFileRoute(
2640
'/organizations/$orgId/products/$productId/repositories/$repoId/_repo-layout/users'
2741
)({
28-
component: () => <Outlet />,
42+
component: RepositoryUsersGuard,
2943

3044
// Route’s query string parameters (centralized)
3145
validateSearch: paginationSearchParameterSchema,
@@ -52,11 +66,4 @@ export const Route = createFileRoute(
5266
}),
5367
});
5468
},
55-
beforeLoad: ({ context }) => {
56-
if (!context.permissions.repository?.includes('MANAGE_GROUPS')) {
57-
throw redirect({
58-
to: '/403',
59-
});
60-
}
61-
},
6269
});

ui/src/routes/organizations/$orgId/products/$productId/secrets/route.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,25 @@
1717
* License-Filename: LICENSE
1818
*/
1919

20-
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
20+
import { createFileRoute, Outlet } from '@tanstack/react-router';
21+
22+
import { RequireProductPermission } from '@/components/authorization';
23+
24+
function ProductSecretsGuard() {
25+
const { productId } = Route.useParams();
26+
27+
return (
28+
<RequireProductPermission
29+
productId={Number.parseInt(productId)}
30+
permission='WRITE_SECRETS'
31+
>
32+
<Outlet />
33+
</RequireProductPermission>
34+
);
35+
}
2136

2237
export const Route = createFileRoute(
2338
'/organizations/$orgId/products/$productId/secrets'
2439
)({
25-
component: () => <Outlet />,
26-
beforeLoad: ({ context }) => {
27-
if (!context.permissions.product?.includes('WRITE_SECRETS')) {
28-
throw redirect({
29-
to: '/403',
30-
});
31-
}
32-
},
40+
component: ProductSecretsGuard,
3341
});

ui/src/routes/organizations/$orgId/products/$productId/users/route.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,29 @@
1717
* License-Filename: LICENSE
1818
*/
1919

20-
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
20+
import { createFileRoute, Outlet } from '@tanstack/react-router';
2121

2222
import { getProductUsersOptions } from '@/api/@tanstack/react-query.gen';
23+
import { RequireProductPermission } from '@/components/authorization';
2324
import { paginationSearchParameterSchema } from '@/schemas';
2425

26+
function ProductUsersGuard() {
27+
const { productId } = Route.useParams();
28+
29+
return (
30+
<RequireProductPermission
31+
productId={Number.parseInt(productId)}
32+
permission='MANAGE_GROUPS'
33+
>
34+
<Outlet />
35+
</RequireProductPermission>
36+
);
37+
}
38+
2539
export const Route = createFileRoute(
2640
'/organizations/$orgId/products/$productId/users'
2741
)({
28-
component: () => <Outlet />,
42+
component: ProductUsersGuard,
2943

3044
// Route’s query string parameters (centralized)
3145
validateSearch: paginationSearchParameterSchema,
@@ -52,11 +66,4 @@ export const Route = createFileRoute(
5266
}),
5367
});
5468
},
55-
beforeLoad: ({ context }) => {
56-
if (!context.permissions.product?.includes('MANAGE_GROUPS')) {
57-
throw redirect({
58-
to: '/403',
59-
});
60-
}
61-
},
6269
});

ui/src/routes/organizations/$orgId/secrets/route.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,23 @@
1717
* License-Filename: LICENSE
1818
*/
1919

20-
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
20+
import { createFileRoute, Outlet } from '@tanstack/react-router';
21+
22+
import { RequireOrganizationPermission } from '@/components/authorization';
23+
24+
function OrganizationSecretsGuard() {
25+
const { orgId } = Route.useParams();
26+
27+
return (
28+
<RequireOrganizationPermission
29+
organizationId={Number.parseInt(orgId)}
30+
permission='WRITE_SECRETS'
31+
>
32+
<Outlet />
33+
</RequireOrganizationPermission>
34+
);
35+
}
2136

2237
export const Route = createFileRoute('/organizations/$orgId/secrets')({
23-
component: () => <Outlet />,
24-
beforeLoad: ({ context }) => {
25-
if (!context.permissions.organization?.includes('WRITE_SECRETS')) {
26-
throw redirect({
27-
to: '/403',
28-
});
29-
}
30-
},
38+
component: OrganizationSecretsGuard,
3139
});

ui/src/routes/organizations/$orgId/users/route.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,27 @@
1717
* License-Filename: LICENSE
1818
*/
1919

20-
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
20+
import { createFileRoute, Outlet } from '@tanstack/react-router';
2121

2222
import { getOrganizationUsersOptions } from '@/api/@tanstack/react-query.gen';
23+
import { RequireOrganizationPermission } from '@/components/authorization';
2324
import { paginationSearchParameterSchema } from '@/schemas';
2425

26+
function OrganizationUsersGuard() {
27+
const { orgId } = Route.useParams();
28+
29+
return (
30+
<RequireOrganizationPermission
31+
organizationId={Number.parseInt(orgId)}
32+
permission='MANAGE_GROUPS'
33+
>
34+
<Outlet />
35+
</RequireOrganizationPermission>
36+
);
37+
}
38+
2539
export const Route = createFileRoute('/organizations/$orgId/users')({
26-
component: () => <Outlet />,
40+
component: OrganizationUsersGuard,
2741

2842
// Route’s query string parameters (centralized)
2943
validateSearch: paginationSearchParameterSchema,
@@ -50,11 +64,4 @@ export const Route = createFileRoute('/organizations/$orgId/users')({
5064
}),
5165
});
5266
},
53-
beforeLoad: ({ context }) => {
54-
if (!context.permissions.organization?.includes('MANAGE_GROUPS')) {
55-
throw redirect({
56-
to: '/403',
57-
});
58-
}
59-
},
6067
});

0 commit comments

Comments
 (0)