Skip to content

Commit bca4f83

Browse files
authored
feat(auth): hide inaccessible resource buttons (#242)
1 parent e4c9999 commit bca4f83

5 files changed

Lines changed: 52 additions & 42 deletions

File tree

src/components/presentation/dashboard-button.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import type { Route } from "next";
44

55
import { Link } from "@/components/core/link";
66
import { Button } from "@/components/ui/button";
7+
import { permit } from "@/features/authentication/server";
8+
import type { RoutePermission } from "@/features/authentication/types";
79
import { GrammaticalCase, declineNoun } from "@/features/polish";
810
import { getManagingResourceLabel } from "@/features/resources";
911
import type { RoutableResource } from "@/features/resources/types";
1012
import { cn } from "@/lib/utils";
1113
import { toTitleCase } from "@/utils";
1214

13-
export function DashboardButton({
15+
export async function DashboardButton({
1416
icon: Icon,
1517
variant = "default",
1618
className,
@@ -31,18 +33,18 @@ export function DashboardButton({
3133
preserveCase?: boolean;
3234
}
3335
| {
34-
href: Route;
36+
href: Route & RoutePermission;
3537
label: string;
3638
resource?: never;
3739
longLabel?: never;
3840
preserveCase?: never;
3941
}
4042
)) {
41-
const [href, label] =
43+
const [route, label] =
4244
resource == null
4345
? [hrefOverride, labelOverride]
4446
: [
45-
hrefOverride ?? (`/${resource}` as const),
47+
`/${resource}` as const,
4648
longLabel
4749
? getManagingResourceLabel(resource)
4850
: (labelOverride ??
@@ -52,6 +54,11 @@ export function DashboardButton({
5254
})),
5355
];
5456

57+
const hasPermission = await permit(route);
58+
if (!hasPermission) {
59+
return null;
60+
}
61+
5562
const useViewTransition = resource != null && variant === "default";
5663

5764
return (
@@ -64,7 +71,7 @@ export function DashboardButton({
6471
asChild
6572
>
6673
<Link
67-
href={href}
74+
href={hrefOverride ?? route}
6875
style={{
6976
viewTransitionName: useViewTransition
7077
? `resource-card-${resource}`

src/features/authentication/data/route-permissions.ts

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,42 @@ import { Resource } from "@/features/resources";
44
import type { RoutableResource } from "@/features/resources/types";
55
import type { RecordIntersection } from "@/types/helpers";
66

7+
const SOLVRO_ADMINS_ONLY = ["solvro_admin"] as const;
8+
const ADMINS_ONLY = ["admin", ...SOLVRO_ADMINS_ONLY] as const;
9+
const ANY_AUTHENTICATED_ROLE = ["user", ...ADMINS_ONLY] as const;
10+
711
export const ROUTE_PERMISSIONS = {
8-
"/": ["user", "admin"],
9-
"/review": ["user", "admin"],
10-
[`/${Resource.AboutUs}`]: ["user", "admin"],
11-
[`/${Resource.AboutUsLinks}`]: ["user", "admin"],
12-
[`/${Resource.AcademicSemesters}`]: ["user", "admin"],
13-
[`/${Resource.Aeds}`]: ["user", "admin"],
14-
[`/${Resource.Banners}`]: ["user", "admin"],
15-
[`/${Resource.BicycleShowers}`]: ["user", "admin"],
16-
[`/${Resource.Buildings}`]: ["user", "admin"],
17-
[`/${Resource.CalendarEvents}`]: ["user", "admin"],
18-
[`/${Resource.Campuses}`]: ["user", "admin"],
19-
[`/${Resource.Contributors}`]: ["user", "admin"],
20-
[`/${Resource.DaySwaps}`]: ["user", "admin"],
21-
[`/${Resource.Departments}`]: ["user", "admin"],
22-
[`/${Resource.FoodSpots}`]: ["user", "admin"],
23-
[`/${Resource.GuideArticles}`]: ["user", "admin"],
24-
[`/${Resource.Holidays}`]: ["user", "admin"],
25-
[`/${Resource.Libraries}`]: ["user", "admin"],
26-
[`/${Resource.Map}`]: ["user", "admin"],
27-
[`/${Resource.Milestones}`]: ["user", "admin"],
28-
[`/${Resource.MobileConfig}`]: ["user", "admin"],
29-
[`/${Resource.Notifications}`]: ["user", "admin"],
30-
[`/${Resource.NotificationTopics}`]: ["user", "admin"],
31-
[`/${Resource.PinkBoxes}`]: ["user", "admin"],
32-
[`/${Resource.PolinkaStations}`]: ["user", "admin"],
33-
[`/${Resource.Roles}`]: ["user", "admin"],
34-
[`/${Resource.SksOpeningHours}`]: ["user", "admin"],
35-
[`/${Resource.StudentOrganizations}`]: ["user", "admin"],
36-
[`/${Resource.Versions}`]: ["user", "admin"],
12+
"/": ANY_AUTHENTICATED_ROLE,
13+
"/review": SOLVRO_ADMINS_ONLY,
14+
[`/${Resource.AboutUs}`]: ANY_AUTHENTICATED_ROLE,
15+
[`/${Resource.AboutUsLinks}`]: ANY_AUTHENTICATED_ROLE,
16+
[`/${Resource.AcademicSemesters}`]: ANY_AUTHENTICATED_ROLE,
17+
[`/${Resource.Aeds}`]: ANY_AUTHENTICATED_ROLE,
18+
[`/${Resource.Banners}`]: ANY_AUTHENTICATED_ROLE,
19+
[`/${Resource.BicycleShowers}`]: ANY_AUTHENTICATED_ROLE,
20+
[`/${Resource.Buildings}`]: ANY_AUTHENTICATED_ROLE,
21+
[`/${Resource.CalendarEvents}`]: ANY_AUTHENTICATED_ROLE,
22+
[`/${Resource.Campuses}`]: ANY_AUTHENTICATED_ROLE,
23+
[`/${Resource.Contributors}`]: ANY_AUTHENTICATED_ROLE,
24+
[`/${Resource.DaySwaps}`]: ANY_AUTHENTICATED_ROLE,
25+
[`/${Resource.Departments}`]: ANY_AUTHENTICATED_ROLE,
26+
[`/${Resource.FoodSpots}`]: ANY_AUTHENTICATED_ROLE,
27+
[`/${Resource.GuideArticles}`]: ANY_AUTHENTICATED_ROLE,
28+
[`/${Resource.Holidays}`]: ANY_AUTHENTICATED_ROLE,
29+
[`/${Resource.Libraries}`]: ANY_AUTHENTICATED_ROLE,
30+
[`/${Resource.Map}`]: ANY_AUTHENTICATED_ROLE,
31+
[`/${Resource.Milestones}`]: ANY_AUTHENTICATED_ROLE,
32+
[`/${Resource.MobileConfig}`]: ANY_AUTHENTICATED_ROLE,
33+
[`/${Resource.Notifications}`]: ANY_AUTHENTICATED_ROLE,
34+
[`/${Resource.NotificationTopics}`]: ANY_AUTHENTICATED_ROLE,
35+
[`/${Resource.PinkBoxes}`]: ANY_AUTHENTICATED_ROLE,
36+
[`/${Resource.PolinkaStations}`]: ANY_AUTHENTICATED_ROLE,
37+
[`/${Resource.Roles}`]: ANY_AUTHENTICATED_ROLE,
38+
[`/${Resource.SksOpeningHours}`]: ANY_AUTHENTICATED_ROLE,
39+
[`/${Resource.StudentOrganizations}`]: ANY_AUTHENTICATED_ROLE,
40+
[`/${Resource.Versions}`]: ANY_AUTHENTICATED_ROLE,
3741
} satisfies RecordIntersection<
3842
`/${RoutableResource}`,
3943
Route,
40-
string[] | undefined
44+
readonly string[] | undefined
4145
>;

src/features/authentication/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from "./components/private-layout";
33
export * from "./components/public-layout";
44

55
export * from "./utils/get-auth-state.server";
6+
export * from "./utils/permit";

src/features/authentication/utils/get-route-permissions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@ import type { RoutePermission } from "../types/internal";
66
* The route permissions configuration is typed to include all routable resources, so this is enforced by TypeScript,
77
* but during development it helps by displaying a 403 Forbidden error rather than a runtime error in case of missing routes.
88
*/
9-
export const getRoutePermissions = (route: RoutePermission): string[] =>
10-
(ROUTE_PERMISSIONS[route] as string[] | undefined) ?? [];
9+
export const getRoutePermissions = (
10+
route: RoutePermission,
11+
): readonly string[] =>
12+
(ROUTE_PERMISSIONS[route] as readonly string[] | undefined) ?? [];

src/features/authentication/utils/get-user-permissions.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ export function getUserPermissions(user: User | null): string[] {
44
if (user == null) {
55
return [];
66
}
7-
// TODO: update this when the backend supports user roles
8-
const roles = ["user"];
9-
// if (user.email.endsWith("@solvro.pl")) {
10-
// roles.push("admin");
11-
// }
7+
const roles = ["user", ...user.roles.map((role) => role.slug)];
128
return roles;
139
}

0 commit comments

Comments
 (0)