@shivaedev/react 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +75 -0
- package/dist/client/permissionBoundary.d.ts +11 -0
- package/dist/client/permissionBoundary.d.ts.map +1 -0
- package/dist/client/permissionBoundary.js +20 -0
- package/dist/client/permissionBoundary.js.map +1 -0
- package/dist/client/resultState.d.ts.map +1 -1
- package/dist/client/resultState.js +4 -1
- package/dist/client/resultState.js.map +1 -1
- package/dist/internal/PermissionContext.d.ts +7 -0
- package/dist/internal/PermissionContext.d.ts.map +1 -0
- package/dist/internal/PermissionContext.js +3 -0
- package/dist/internal/PermissionContext.js.map +1 -0
- package/dist/internal/usePermissionFailure.d.ts +3 -0
- package/dist/internal/usePermissionFailure.d.ts.map +1 -0
- package/dist/internal/usePermissionFailure.js +13 -0
- package/dist/internal/usePermissionFailure.js.map +1 -0
- package/dist/permissionGuard.d.ts +22 -0
- package/dist/permissionGuard.d.ts.map +1 -0
- package/dist/permissionGuard.js +21 -0
- package/dist/permissionGuard.js.map +1 -0
- package/dist/permissions.d.ts +8 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +4 -0
- package/dist/permissions.js.map +1 -0
- package/package.json +10 -8
- package/src/client/permissionBoundary.ts +33 -0
- package/src/client/resultState.ts +4 -1
- package/src/internal/PermissionContext.ts +6 -0
- package/src/internal/usePermissionFailure.ts +13 -0
- package/src/permissionGuard.ts +39 -0
- package/src/permissions.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.20.0 - 2026-10-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Read session grants with `can` and `useCan`, guard TanStack routes with `staticData.permission` and `permissionGuard`, and render one `PermissionBoundary` fallback for refused routes and forbidden queries. Signed-out route visits redirect to the application's sign-in path with their return location.
|
|
8
|
+
|
|
3
9
|
## 0.19.0 - 2026-10-27
|
|
4
10
|
|
|
5
11
|
### Breaking
|
package/README.md
CHANGED
|
@@ -704,6 +704,81 @@ const query = Atom.swr(atom, {
|
|
|
704
704
|
|
|
705
705
|
`atom` is the underlying native query. `Atom.swr` owns the staleness decision; the signal only reports that the application resumed.
|
|
706
706
|
|
|
707
|
+
### Session permissions and guarded routes
|
|
708
|
+
|
|
709
|
+
Permissions come from the session payload's `user.permissions`, populated by `@shivaedev/access` through the server's role policy. `can(session, permission)` reads exact name and strength grants without making a request. `useCan()` supplies the same check from the nearest `PermissionBoundary` for component controls. A signed-out session has no grants. These checks control visibility; the server still authorizes every request.
|
|
710
|
+
|
|
711
|
+
Declare a route's requirement in TanStack's native `staticData.permission`. Install `permissionGuard` once as the root route's `beforeLoad`; it checks the matched routes before their loaders run. Signed-out visitors go to the app's `signIn` path with the full requested location in the `returnTo` search field. A signed-in session without a required grant throws the shared `Forbidden` failure.
|
|
712
|
+
|
|
713
|
+
```tsx
|
|
714
|
+
import { RegistryContext } from "@effect/atom-react";
|
|
715
|
+
import { createRootRouteWithContext, createRoute, createRouter, RouterProvider } from "@tanstack/react-router";
|
|
716
|
+
import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
717
|
+
import { permission } from "@shivaedev/access/permission.ts";
|
|
718
|
+
import { PermissionBoundary, useCan } from "@shivaedev/react/client/permissionBoundary.ts";
|
|
719
|
+
import { useLoaderQuery } from "@shivaedev/react/client/useLoaderQuery.ts";
|
|
720
|
+
import { permissionGuard } from "@shivaedev/react/permissionGuard.ts";
|
|
721
|
+
import { can, type PermissionSession } from "@shivaedev/react/permissions.ts";
|
|
722
|
+
import { queryRoute } from "@shivaedev/react/queryRoute.ts";
|
|
723
|
+
import { api, registry } from "./notes-client.ts";
|
|
724
|
+
import { auth, SignIn } from "./auth.tsx";
|
|
725
|
+
|
|
726
|
+
const ReadNotes = permission("notes.read");
|
|
727
|
+
const root = createRootRouteWithContext<{ registry: AtomRegistry.AtomRegistry }>()({
|
|
728
|
+
beforeLoad: permissionGuard({ session: () => auth.session, signIn: "/login" }),
|
|
729
|
+
});
|
|
730
|
+
const note = createRoute({
|
|
731
|
+
getParentRoute: () => root,
|
|
732
|
+
path: "/notes/$id",
|
|
733
|
+
staticData: { permission: ReadNotes },
|
|
734
|
+
...queryRoute(({ params }: { params: { id: string }; search: Record<string, never> }) =>
|
|
735
|
+
api.get.query({ id: params.id, page: 1 }),
|
|
736
|
+
),
|
|
737
|
+
component: () => {
|
|
738
|
+
const query = useLoaderQuery(api.get.query({ ...note.useParams(), page: 1 }), note.useLoaderData());
|
|
739
|
+
return <h1>{query.data.title}</h1>;
|
|
740
|
+
},
|
|
741
|
+
});
|
|
742
|
+
const login = createRoute({
|
|
743
|
+
getParentRoute: () => root,
|
|
744
|
+
path: "/login",
|
|
745
|
+
validateSearch: (search: Record<string, unknown>) => ({
|
|
746
|
+
returnTo: typeof search.returnTo === "string" ? search.returnTo : "/",
|
|
747
|
+
}),
|
|
748
|
+
component: () => {
|
|
749
|
+
const { returnTo } = login.useSearch();
|
|
750
|
+
return <SignIn onSignedIn={() => router.navigate({ href: returnTo })} />;
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
const router = createRouter({
|
|
754
|
+
context: { registry },
|
|
755
|
+
routeTree: root.addChildren([note, login]),
|
|
756
|
+
disableGlobalCatchBoundary: true,
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
function Controls() {
|
|
760
|
+
const can = useCan();
|
|
761
|
+
return can(ReadNotes) ? <button type="button">Read notes</button> : null;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
export function App({ session }: { readonly session: PermissionSession | undefined }) {
|
|
765
|
+
return (
|
|
766
|
+
<PermissionBoundary session={session} fallback={<h1>Permission denied</h1>}>
|
|
767
|
+
<RegistryContext.Provider value={registry}>
|
|
768
|
+
<Controls />
|
|
769
|
+
<RouterProvider router={router} />
|
|
770
|
+
</RegistryContext.Provider>
|
|
771
|
+
</PermissionBoundary>
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const showNotes = can(auth.session, ReadNotes);
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
`auth` owns the current session and passes it to `App`; `SignIn` updates that session before invoking `onSignedIn`. Use the session's registry for router context and React, as with other query routes. Importing `permissionGuard` augments TanStack's `StaticDataRouteOption`; route options and their inference remain native.
|
|
779
|
+
|
|
780
|
+
`PermissionBoundary` renders the app's one fallback at the denied URL for a refused route, a loader's `Forbidden`, or a `useQuery`/`useLoaderQuery` result tagged `Forbidden`. Other errors propagate to an enclosing error boundary. Set TanStack's `disableGlobalCatchBoundary: true` and leave permission failures to this boundary; a route-specific `errorComponent` that consumes an error owns its rendering instead. Without a permission boundary, query failures keep their normal typed state. Like a React error boundary, remount it to clear a caught failure.
|
|
781
|
+
|
|
707
782
|
### API
|
|
708
783
|
|
|
709
784
|
`useQuery(atom)` and `useAction(atom)` return these shared fields:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { Permission } from "@shivaedev/access/permission.ts";
|
|
3
|
+
import { type PermissionSession } from "#permissions.ts";
|
|
4
|
+
export interface PermissionBoundaryProps {
|
|
5
|
+
readonly children: ReactNode;
|
|
6
|
+
readonly fallback: ReactNode;
|
|
7
|
+
readonly session: PermissionSession | null | undefined;
|
|
8
|
+
}
|
|
9
|
+
export declare function useCan(): (permission: Permission) => boolean;
|
|
10
|
+
export declare function PermissionBoundary({ children, fallback, session }: PermissionBoundaryProps): ReactNode;
|
|
11
|
+
//# sourceMappingURL=permissionBoundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionBoundary.d.ts","sourceRoot":"","sources":["../../src/client/permissionBoundary.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,SAAS,EAAc,MAAM,OAAO,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,OAAO,EAAO,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAE9D,MAAM,WAAW,uBAAuB;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,CAAC;CACvD;AAED,wBAAgB,MAAM,IAAI,CAAC,UAAU,EAAE,UAAU,KAAK,OAAO,CAG5D;AAUD,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,uBAAuB,GAAG,SAAS,CAMtG"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CatchBoundary } from "@tanstack/react-router";
|
|
2
|
+
import { Predicate } from "effect";
|
|
3
|
+
import { createElement, useContext } from "react";
|
|
4
|
+
import { PermissionContext } from "#internal/PermissionContext.ts";
|
|
5
|
+
import { can } from "#permissions.ts";
|
|
6
|
+
export function useCan() {
|
|
7
|
+
const context = useContext(PermissionContext);
|
|
8
|
+
return (permission) => can(context?.session, permission);
|
|
9
|
+
}
|
|
10
|
+
function PermissionFailure({ error }) {
|
|
11
|
+
const context = useContext(PermissionContext);
|
|
12
|
+
if (!Predicate.isTagged(error, "Forbidden")) {
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
return context?.fallback;
|
|
16
|
+
}
|
|
17
|
+
export function PermissionBoundary({ children, fallback, session }) {
|
|
18
|
+
return createElement(PermissionContext.Provider, { value: { fallback, session } }, createElement(CatchBoundary, { children, errorComponent: PermissionFailure, getResetKey: () => "permission" }));
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=permissionBoundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionBoundary.js","sourceRoot":"","sources":["../../src/client/permissionBoundary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EAAE,aAAa,EAAkB,UAAU,EAAE,MAAM,OAAO,CAAC;AAElE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,GAAG,EAA0B,MAAM,iBAAiB,CAAC;AAQ9D,MAAM,UAAU,MAAM;IACrB,MAAM,OAAO,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAC9C,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,iBAAiB,CAAC,EAAE,KAAK,EAA+B;IAChE,MAAM,OAAO,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAC9C,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,EAAE,CAAC;QAC7C,MAAM,KAAK,CAAC;IACb,CAAC;IACD,OAAO,OAAO,EAAE,QAAQ,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAA2B;IAC1F,OAAO,aAAa,CACnB,iBAAiB,CAAC,QAAQ,EAC1B,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,EAChC,aAAa,CAAC,aAAa,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,YAAY,EAAE,CAAC,CAC9G,CAAC;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resultState.d.ts","sourceRoot":"","sources":["../../src/client/resultState.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,EAAa,MAAM,QAAQ,CAAC;AAC9E,OAAO,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAC7D,OAAO,KAAK,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAIpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"resultState.d.ts","sourceRoot":"","sources":["../../src/client/resultState.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
|
4
4
|
import { useContext, useEffect, useState } from "react";
|
|
5
5
|
import { queryCoalesceWindow } from "@shivaedev/contract/queryCoalesceWindow.ts";
|
|
6
6
|
import { acceptLoaderQuery } from "#internal/queryHandoff.ts";
|
|
7
|
+
import { usePermissionFailure } from "#internal/usePermissionFailure.ts";
|
|
7
8
|
import { useQueryNotice } from "#internal/useQueryNotice.ts";
|
|
8
9
|
import { useActionSource } from "./actionCompletion.js";
|
|
9
10
|
import { useSessionRecheck } from "./sessionBoundary.js";
|
|
@@ -51,7 +52,9 @@ export function useQuery(atom, options = {}) {
|
|
|
51
52
|
}
|
|
52
53
|
useRecheckOnUnauthorized(result);
|
|
53
54
|
useQueryNotice(result, Option.isSome(data), options.notice);
|
|
54
|
-
|
|
55
|
+
const refresh = useAtomRefresh(atom);
|
|
56
|
+
usePermissionFailure(AsyncResult.cause(result));
|
|
57
|
+
return { ...state(result, data), refresh };
|
|
55
58
|
}
|
|
56
59
|
export function useAction(source, run, options = {}) {
|
|
57
60
|
const atom = useActionSource(source, run, options);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resultState.js","sourceRoot":"","sources":["../../src/client/resultState.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"resultState.js","sourceRoot":"","sources":["../../src/client/resultState.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { PermissionSession } from "#permissions.ts";
|
|
3
|
+
export declare const PermissionContext: import("react").Context<{
|
|
4
|
+
readonly fallback: ReactNode;
|
|
5
|
+
readonly session: PermissionSession | null | undefined;
|
|
6
|
+
} | undefined>;
|
|
7
|
+
//# sourceMappingURL=PermissionContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PermissionContext.d.ts","sourceRoot":"","sources":["../../src/internal/PermissionContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,eAAO,MAAM,iBAAiB;uBAAsC,SAAS;sBAAoB,iBAAiB,GAAG,IAAI,GAAG,SAAS;cAEpI,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PermissionContext.js","sourceRoot":"","sources":["../../src/internal/PermissionContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAkB,MAAM,OAAO,CAAC;AAGtD,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAC7C,SAAS,CACT,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePermissionFailure.d.ts","sourceRoot":"","sources":["../../src/internal/usePermissionFailure.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAa,MAAM,QAAQ,CAAC;AAIlD,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAQlF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Cause, Option, Predicate } from "effect";
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
import { PermissionContext } from "./PermissionContext.js";
|
|
4
|
+
export function usePermissionFailure(cause) {
|
|
5
|
+
const context = useContext(PermissionContext);
|
|
6
|
+
if (context !== undefined) {
|
|
7
|
+
const failure = Option.flatMap(cause, Cause.findErrorOption);
|
|
8
|
+
if (Option.isSome(failure) && Predicate.isTagged(failure.value, "Forbidden")) {
|
|
9
|
+
throw failure.value;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=usePermissionFailure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePermissionFailure.js","sourceRoot":"","sources":["../../src/internal/usePermissionFailure.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D,MAAM,UAAU,oBAAoB,CAAI,KAAoC;IAC3E,MAAM,OAAO,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAC9C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;QAC7D,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,WAAW,CAAC,EAAE,CAAC;YAC9E,MAAM,OAAO,CAAC,KAAK,CAAC;QACrB,CAAC;IACF,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Permission } from "@shivaedev/access/permission.ts";
|
|
2
|
+
import { type PermissionSession } from "./permissions.ts";
|
|
3
|
+
declare module "@tanstack/react-router" {
|
|
4
|
+
interface StaticDataRouteOption {
|
|
5
|
+
readonly permission?: Permission;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface PermissionGuardOptions {
|
|
9
|
+
readonly session: () => PermissionSession | null | undefined;
|
|
10
|
+
readonly signIn: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function permissionGuard({ session, signIn }: PermissionGuardOptions): ({ matches, location, }: {
|
|
13
|
+
readonly location: {
|
|
14
|
+
readonly href: string;
|
|
15
|
+
};
|
|
16
|
+
readonly matches: readonly {
|
|
17
|
+
readonly staticData: {
|
|
18
|
+
readonly permission?: Permission;
|
|
19
|
+
};
|
|
20
|
+
}[];
|
|
21
|
+
}) => void;
|
|
22
|
+
//# sourceMappingURL=permissionGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionGuard.d.ts","sourceRoot":"","sources":["../src/permissionGuard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,OAAO,EAAO,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE/D,OAAO,QAAQ,wBAAwB,CAAC;IACvC,UAAU,qBAAqB;QAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;KACjC;CACD;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,iBAAiB,GAAG,IAAI,GAAG,SAAS,CAAC;IAC7D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,eAAe,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,sBAAsB,4BAIvE;IACF,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,QAAQ,CAAC,OAAO,EAAE,SAAS;QAAE,QAAQ,CAAC,UAAU,EAAE;YAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAA;SAAE,CAAA;KAAE,EAAE,CAAC;CAC3F,KAAG,IAAI,CAeR"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { redirect } from "@tanstack/react-router";
|
|
2
|
+
import { Forbidden } from "@shivaedev/errors/taxonomy.ts";
|
|
3
|
+
import { can } from "./permissions.js";
|
|
4
|
+
export function permissionGuard({ session, signIn }) {
|
|
5
|
+
return ({ matches, location, }) => {
|
|
6
|
+
const required = matches.flatMap((match) => (match.staticData.permission === undefined ? [] : [match.staticData.permission]));
|
|
7
|
+
if (required.length === 0) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const current = session();
|
|
11
|
+
if (current === null || current === undefined) {
|
|
12
|
+
throw redirect({ search: { returnTo: location.href }, to: signIn });
|
|
13
|
+
}
|
|
14
|
+
for (const permission of required) {
|
|
15
|
+
if (!can(current, permission)) {
|
|
16
|
+
throw new Forbidden({ message: `Permission required: ${permission.name} (${permission.strength})` });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=permissionGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissionGuard.js","sourceRoot":"","sources":["../src/permissionGuard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAElD,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,GAAG,EAA0B,MAAM,kBAAkB,CAAC;AAa/D,MAAM,UAAU,eAAe,CAAC,EAAE,OAAO,EAAE,MAAM,EAA0B;IAC1E,OAAO,CAAC,EACP,OAAO,EACP,QAAQ,GAIR,EAAQ,EAAE;QACV,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC9H,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO;QACR,CAAC;QACD,MAAM,OAAO,GAAG,OAAO,EAAE,CAAC;QAC1B,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC/C,MAAM,QAAQ,CAAC,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QACrE,CAAC;QACD,KAAK,MAAM,UAAU,IAAI,QAAQ,EAAE,CAAC;YACnC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC;gBAC/B,MAAM,IAAI,SAAS,CAAC,EAAE,OAAO,EAAE,wBAAwB,UAAU,CAAC,IAAI,KAAK,UAAU,CAAC,QAAQ,GAAG,EAAE,CAAC,CAAC;YACtG,CAAC;QACF,CAAC;IACF,CAAC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Permission } from "@shivaedev/access/permission.ts";
|
|
2
|
+
export interface PermissionSession {
|
|
3
|
+
readonly user: {
|
|
4
|
+
readonly permissions?: readonly Permission[];
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export declare function can(session: PermissionSession | null | undefined, permission: Permission): boolean;
|
|
8
|
+
//# sourceMappingURL=permissions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,IAAI,EAAE;QACd,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;KAC7C,CAAC;CACF;AAED,wBAAgB,GAAG,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,EAAE,UAAU,EAAE,UAAU,GAAG,OAAO,CAElG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.js","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAQA,MAAM,UAAU,GAAG,CAAC,OAA6C,EAAE,UAAsB;IACxF,OAAO,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC,IAAI,IAAI,KAAK,CAAC,QAAQ,KAAK,UAAU,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC;AACtI,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "React query, action, editor and session hooks for native Effect atoms",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect"
|
|
@@ -46,7 +46,9 @@
|
|
|
46
46
|
"README.md"
|
|
47
47
|
],
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@shivaedev/
|
|
49
|
+
"@shivaedev/access": "0.1.1",
|
|
50
|
+
"@shivaedev/contract": "0.18.0",
|
|
51
|
+
"@shivaedev/errors": "0.1.0",
|
|
50
52
|
"@shivaedev/form": "0.7.0",
|
|
51
53
|
"@shivaedev/test": "0.10.0"
|
|
52
54
|
},
|
|
@@ -55,15 +57,15 @@
|
|
|
55
57
|
"@effect/platform-node": "4.0.5",
|
|
56
58
|
"@effect/sql-pg": "4.0.5",
|
|
57
59
|
"@effect/sql-sqlite-node": "4.0.5",
|
|
58
|
-
"@shivaedev/identity": "0.
|
|
59
|
-
"@shivaedev/jobs": "0.
|
|
60
|
+
"@shivaedev/identity": "0.2.0",
|
|
61
|
+
"@shivaedev/jobs": "0.21.0",
|
|
60
62
|
"@shivaedev/local-postgres": "0.7.0",
|
|
61
63
|
"@shivaedev/quality": "0.42.0",
|
|
62
|
-
"@shivaedev/server": "0.
|
|
64
|
+
"@shivaedev/server": "0.2.0",
|
|
63
65
|
"@shivaedev/service": "0.5.0",
|
|
64
|
-
"@shivaedev/sql": "0.
|
|
65
|
-
"@shivaedev/test-sql": "0.10.
|
|
66
|
-
"@shivaedev/test-story": "0.21.
|
|
66
|
+
"@shivaedev/sql": "0.23.0",
|
|
67
|
+
"@shivaedev/test-sql": "0.10.1",
|
|
68
|
+
"@shivaedev/test-story": "0.21.1",
|
|
67
69
|
"@tanstack/react-router": "1.170.45",
|
|
68
70
|
"@types/node": "24.19.2",
|
|
69
71
|
"@types/react": "19.3.0",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CatchBoundary } from "@tanstack/react-router";
|
|
2
|
+
import { Predicate } from "effect";
|
|
3
|
+
import { createElement, type ReactNode, useContext } from "react";
|
|
4
|
+
import type { Permission } from "@shivaedev/access/permission.ts";
|
|
5
|
+
import { PermissionContext } from "#internal/PermissionContext.ts";
|
|
6
|
+
import { can, type PermissionSession } from "#permissions.ts";
|
|
7
|
+
|
|
8
|
+
export interface PermissionBoundaryProps {
|
|
9
|
+
readonly children: ReactNode;
|
|
10
|
+
readonly fallback: ReactNode;
|
|
11
|
+
readonly session: PermissionSession | null | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useCan(): (permission: Permission) => boolean {
|
|
15
|
+
const context = useContext(PermissionContext);
|
|
16
|
+
return (permission) => can(context?.session, permission);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function PermissionFailure({ error }: { readonly error: unknown }): ReactNode {
|
|
20
|
+
const context = useContext(PermissionContext);
|
|
21
|
+
if (!Predicate.isTagged(error, "Forbidden")) {
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
return context?.fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function PermissionBoundary({ children, fallback, session }: PermissionBoundaryProps): ReactNode {
|
|
28
|
+
return createElement(
|
|
29
|
+
PermissionContext.Provider,
|
|
30
|
+
{ value: { fallback, session } },
|
|
31
|
+
createElement(CatchBoundary, { children, errorComponent: PermissionFailure, getResetKey: () => "permission" }),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -7,6 +7,7 @@ import { useContext, useEffect, useState } from "react";
|
|
|
7
7
|
import { queryCoalesceWindow } from "@shivaedev/contract/queryCoalesceWindow.ts";
|
|
8
8
|
import type { AtomServices } from "#fieldRejection.ts";
|
|
9
9
|
import { acceptLoaderQuery } from "#internal/queryHandoff.ts";
|
|
10
|
+
import { usePermissionFailure } from "#internal/usePermissionFailure.ts";
|
|
10
11
|
import { useQueryNotice } from "#internal/useQueryNotice.ts";
|
|
11
12
|
import type { NoticeDescriptor } from "#notices.ts";
|
|
12
13
|
import { type ActionOptions, useActionSource } from "./actionCompletion.ts";
|
|
@@ -79,7 +80,9 @@ export function useQuery<A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>, o
|
|
|
79
80
|
}
|
|
80
81
|
useRecheckOnUnauthorized(result);
|
|
81
82
|
useQueryNotice(result, Option.isSome(data), options.notice);
|
|
82
|
-
|
|
83
|
+
const refresh = useAtomRefresh(atom);
|
|
84
|
+
usePermissionFailure(AsyncResult.cause(result));
|
|
85
|
+
return { ...state(result, data), refresh };
|
|
83
86
|
}
|
|
84
87
|
export interface ActionState<TInput, A, E> extends ResultState<A, E> {
|
|
85
88
|
readonly dispatch: (input: TInput) => void;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContext, type ReactNode } from "react";
|
|
2
|
+
import type { PermissionSession } from "#permissions.ts";
|
|
3
|
+
|
|
4
|
+
export const PermissionContext = createContext<{ readonly fallback: ReactNode; readonly session: PermissionSession | null | undefined } | undefined>(
|
|
5
|
+
undefined,
|
|
6
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Cause, Option, Predicate } from "effect";
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
import { PermissionContext } from "./PermissionContext.ts";
|
|
4
|
+
|
|
5
|
+
export function usePermissionFailure<E>(cause: Option.Option<Cause.Cause<E>>): void {
|
|
6
|
+
const context = useContext(PermissionContext);
|
|
7
|
+
if (context !== undefined) {
|
|
8
|
+
const failure = Option.flatMap(cause, Cause.findErrorOption);
|
|
9
|
+
if (Option.isSome(failure) && Predicate.isTagged(failure.value, "Forbidden")) {
|
|
10
|
+
throw failure.value;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { redirect } from "@tanstack/react-router";
|
|
2
|
+
import type { Permission } from "@shivaedev/access/permission.ts";
|
|
3
|
+
import { Forbidden } from "@shivaedev/errors/taxonomy.ts";
|
|
4
|
+
import { can, type PermissionSession } from "./permissions.ts";
|
|
5
|
+
|
|
6
|
+
declare module "@tanstack/react-router" {
|
|
7
|
+
interface StaticDataRouteOption {
|
|
8
|
+
readonly permission?: Permission;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PermissionGuardOptions {
|
|
13
|
+
readonly session: () => PermissionSession | null | undefined;
|
|
14
|
+
readonly signIn: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function permissionGuard({ session, signIn }: PermissionGuardOptions) {
|
|
18
|
+
return ({
|
|
19
|
+
matches,
|
|
20
|
+
location,
|
|
21
|
+
}: {
|
|
22
|
+
readonly location: { readonly href: string };
|
|
23
|
+
readonly matches: readonly { readonly staticData: { readonly permission?: Permission } }[];
|
|
24
|
+
}): void => {
|
|
25
|
+
const required = matches.flatMap((match) => (match.staticData.permission === undefined ? [] : [match.staticData.permission]));
|
|
26
|
+
if (required.length === 0) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const current = session();
|
|
30
|
+
if (current === null || current === undefined) {
|
|
31
|
+
throw redirect({ search: { returnTo: location.href }, to: signIn });
|
|
32
|
+
}
|
|
33
|
+
for (const permission of required) {
|
|
34
|
+
if (!can(current, permission)) {
|
|
35
|
+
throw new Forbidden({ message: `Permission required: ${permission.name} (${permission.strength})` });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Permission } from "@shivaedev/access/permission.ts";
|
|
2
|
+
|
|
3
|
+
export interface PermissionSession {
|
|
4
|
+
readonly user: {
|
|
5
|
+
readonly permissions?: readonly Permission[];
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function can(session: PermissionSession | null | undefined, permission: Permission): boolean {
|
|
10
|
+
return session?.user.permissions?.some((grant) => grant.name === permission.name && grant.strength === permission.strength) ?? false;
|
|
11
|
+
}
|