@specscreen/backoffice-core 0.1.1
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/README.md +553 -0
- package/dist/index.cjs +1635 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +665 -0
- package/dist/index.d.ts +665 -0
- package/dist/index.js +1565 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +103 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
# `@specscreen/backoffice-core`
|
|
2
|
+
|
|
3
|
+
A reusable backoffice framework with authentication, RBAC, and a consistent admin UI for React / Next.js.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @specscreen/backoffice-core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Include the stylesheet once in your app (Next.js: `app/layout.tsx`, Vite: `main.tsx`):
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import "@specscreen/backoffice-core/styles";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### 1. Implement the providers
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// authProvider.ts
|
|
27
|
+
import type { AuthProvider } from "@specscreen/backoffice-core";
|
|
28
|
+
|
|
29
|
+
export const authProvider: AuthProvider = {
|
|
30
|
+
async login({ email, password }) {
|
|
31
|
+
const res = await fetch("/api/auth/login", {
|
|
32
|
+
method: "POST",
|
|
33
|
+
body: JSON.stringify({ email, password }),
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) throw new Error("Invalid credentials");
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async logout() {
|
|
39
|
+
await fetch("/api/auth/logout", { method: "POST" });
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async checkAuth() {
|
|
43
|
+
const res = await fetch("/api/auth/me");
|
|
44
|
+
return res.ok;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async getUser() {
|
|
48
|
+
const res = await fetch("/api/auth/me");
|
|
49
|
+
if (!res.ok) return null;
|
|
50
|
+
return res.json(); // { id, email, roles, permissions }
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// dataProvider.ts
|
|
57
|
+
import type { DataProvider } from "@specscreen/backoffice-core";
|
|
58
|
+
|
|
59
|
+
export const dataProvider: DataProvider = {
|
|
60
|
+
async getList(resource, params) {
|
|
61
|
+
const res = await fetch(`/api/${resource}`);
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
return { data, total: data.length };
|
|
64
|
+
},
|
|
65
|
+
async getOne(resource, id) {
|
|
66
|
+
const res = await fetch(`/api/${resource}/${id}`);
|
|
67
|
+
return res.json();
|
|
68
|
+
},
|
|
69
|
+
async create(resource, data) {
|
|
70
|
+
const res = await fetch(`/api/${resource}`, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
body: JSON.stringify(data),
|
|
73
|
+
});
|
|
74
|
+
return res.json();
|
|
75
|
+
},
|
|
76
|
+
async update(resource, id, data) {
|
|
77
|
+
const res = await fetch(`/api/${resource}/${id}`, {
|
|
78
|
+
method: "PUT",
|
|
79
|
+
body: JSON.stringify(data),
|
|
80
|
+
});
|
|
81
|
+
return res.json();
|
|
82
|
+
},
|
|
83
|
+
async delete(resource, id) {
|
|
84
|
+
await fetch(`/api/${resource}/${id}`, { method: "DELETE" });
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 2. Define your config
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// config.ts
|
|
93
|
+
import { Users, FileText, Settings } from "lucide-react";
|
|
94
|
+
import type { BackofficeConfig } from "@specscreen/backoffice-core";
|
|
95
|
+
|
|
96
|
+
export const config: BackofficeConfig = {
|
|
97
|
+
appName: "My Backoffice",
|
|
98
|
+
|
|
99
|
+
sidebarGroups: [
|
|
100
|
+
{
|
|
101
|
+
label: "Platform",
|
|
102
|
+
resources: [
|
|
103
|
+
{
|
|
104
|
+
name: "users",
|
|
105
|
+
label: "Users",
|
|
106
|
+
path: "/users",
|
|
107
|
+
icon: Users,
|
|
108
|
+
list: UsersPage,
|
|
109
|
+
meta: {
|
|
110
|
+
requiredPermissions: ["users.read"],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "posts",
|
|
115
|
+
label: "Posts",
|
|
116
|
+
path: "/posts",
|
|
117
|
+
icon: FileText,
|
|
118
|
+
list: PostsPage,
|
|
119
|
+
meta: {
|
|
120
|
+
requiredRoles: ["editor", "admin"],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
|
|
127
|
+
sidebarFooterLinks: [
|
|
128
|
+
{ label: "Settings", path: "/settings", icon: Settings },
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 3. Mount the app
|
|
134
|
+
|
|
135
|
+
#### Next.js App Router (`app/layout.tsx`)
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
import {
|
|
139
|
+
BackofficeApp,
|
|
140
|
+
type BackofficeAppProps,
|
|
141
|
+
} from "@specscreen/backoffice-core";
|
|
142
|
+
import "@specscreen/backoffice-core/styles";
|
|
143
|
+
import Link from "next/link";
|
|
144
|
+
import { usePathname } from "next/navigation";
|
|
145
|
+
import { authProvider } from "@/lib/authProvider";
|
|
146
|
+
import { dataProvider } from "@/lib/dataProvider";
|
|
147
|
+
import { config } from "@/lib/config";
|
|
148
|
+
|
|
149
|
+
// Adapter: wraps Next.js <Link> into the framework's NavLink shape
|
|
150
|
+
const NavLink = ({ href, children, className }: any) => (
|
|
151
|
+
<Link href={href} className={className}>
|
|
152
|
+
{children}
|
|
153
|
+
</Link>
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
157
|
+
const currentPath = usePathname();
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<html lang="en">
|
|
161
|
+
<body>
|
|
162
|
+
<BackofficeApp
|
|
163
|
+
config={config}
|
|
164
|
+
authProvider={authProvider}
|
|
165
|
+
dataProvider={dataProvider}
|
|
166
|
+
NavLink={NavLink}
|
|
167
|
+
currentPath={currentPath}
|
|
168
|
+
>
|
|
169
|
+
{children}
|
|
170
|
+
</BackofficeApp>
|
|
171
|
+
</body>
|
|
172
|
+
</html>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Vite / React Router
|
|
178
|
+
|
|
179
|
+
```tsx
|
|
180
|
+
import { BackofficeApp } from "@specscreen/backoffice-core";
|
|
181
|
+
import "@specscreen/backoffice-core/styles";
|
|
182
|
+
import { Link, useLocation, Outlet } from "react-router-dom";
|
|
183
|
+
import { authProvider } from "./lib/authProvider";
|
|
184
|
+
import { dataProvider } from "./lib/dataProvider";
|
|
185
|
+
import { config } from "./lib/config";
|
|
186
|
+
|
|
187
|
+
const NavLink = ({ href, children, className }: any) => (
|
|
188
|
+
<Link to={href} className={className}>
|
|
189
|
+
{children}
|
|
190
|
+
</Link>
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
export function App() {
|
|
194
|
+
const { pathname } = useLocation();
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<BackofficeApp
|
|
198
|
+
config={config}
|
|
199
|
+
authProvider={authProvider}
|
|
200
|
+
dataProvider={dataProvider}
|
|
201
|
+
NavLink={NavLink}
|
|
202
|
+
currentPath={pathname}
|
|
203
|
+
>
|
|
204
|
+
<Outlet />
|
|
205
|
+
</BackofficeApp>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Authentication
|
|
213
|
+
|
|
214
|
+
### `AuthProvider` interface
|
|
215
|
+
|
|
216
|
+
| Method | Signature | Description |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `login` | `({ email, password }) → Promise<void>` | Throw on failure |
|
|
219
|
+
| `logout` | `() → Promise<void>` | Clear session |
|
|
220
|
+
| `checkAuth` | `() → Promise<boolean>` | Return `false` (never throw) on invalid session |
|
|
221
|
+
| `getUser` | `() → Promise<User \| null>` | Return `null` if unauthenticated |
|
|
222
|
+
|
|
223
|
+
### Boot flow
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
App mount
|
|
227
|
+
→ checkAuth() true → getUser() → render app
|
|
228
|
+
false → render <LoginPage />
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### `useAuth` hook
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
import { useAuth } from "@specscreen/backoffice-core";
|
|
235
|
+
|
|
236
|
+
function MyComponent() {
|
|
237
|
+
const { user, isAuthenticated, isLoading, login, logout } = useAuth();
|
|
238
|
+
|
|
239
|
+
return <p>Logged in as {user?.email}</p>;
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
| Property | Type | Description |
|
|
244
|
+
|---|---|---|
|
|
245
|
+
| `user` | `User \| null` | Current authenticated user |
|
|
246
|
+
| `isAuthenticated` | `boolean` | Session is active |
|
|
247
|
+
| `isLoading` | `boolean` | Auth boot is in progress |
|
|
248
|
+
| `error` | `string \| null` | Last auth error message |
|
|
249
|
+
| `login` | `(email, password) → Promise<void>` | Delegates to `authProvider.login` |
|
|
250
|
+
| `logout` | `() → Promise<void>` | Delegates to `authProvider.logout` |
|
|
251
|
+
| `refreshUser` | `() → Promise<void>` | Re-fetch user profile |
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## RBAC
|
|
256
|
+
|
|
257
|
+
### Permission model
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
// User object returned by authProvider.getUser()
|
|
261
|
+
{
|
|
262
|
+
id: "u1",
|
|
263
|
+
email: "alice@example.com",
|
|
264
|
+
roles: ["admin", "editor"],
|
|
265
|
+
permissions: ["users.read", "users.create", "posts.publish"]
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
- **Roles** — coarse-grained team/tier (`"admin"`, `"editor"`)
|
|
270
|
+
- **Permissions** — fine-grained actions (`"users.delete"`, `"posts.publish"`)
|
|
271
|
+
- Both are optional strings — the framework never hardcodes meaning
|
|
272
|
+
- Your backend is the source of truth; the UI is decoration only
|
|
273
|
+
|
|
274
|
+
### `usePermissions` hook
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
import { usePermissions } from "@specscreen/backoffice-core";
|
|
278
|
+
|
|
279
|
+
function ActionsBar() {
|
|
280
|
+
const { can, canAny, canAll, hasRole, roles, permissions } = usePermissions();
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div>
|
|
284
|
+
{can("users.create") && <CreateButton />}
|
|
285
|
+
{canAny(["posts.edit", "posts.create"]) && <PostsToolbar />}
|
|
286
|
+
{canAll(["orders.view", "orders.export"]) && <ExportButton />}
|
|
287
|
+
{hasRole("admin") && <AdminSettings />}
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
| Method | Description |
|
|
294
|
+
|---|---|
|
|
295
|
+
| `can(permission)` | User has this specific permission |
|
|
296
|
+
| `canAny(permissions[])` | User has at least one of these |
|
|
297
|
+
| `canAll(permissions[])` | User has all of these |
|
|
298
|
+
| `hasRole(role)` | User has this role |
|
|
299
|
+
| `roles` | `string[]` — all user roles |
|
|
300
|
+
| `permissions` | `string[]` — all user permissions |
|
|
301
|
+
|
|
302
|
+
### `<Can>` guard component
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
import { Can } from "@specscreen/backoffice-core";
|
|
306
|
+
|
|
307
|
+
// Hide when no permission (default)
|
|
308
|
+
<Can permission="users.delete">
|
|
309
|
+
<DeleteButton />
|
|
310
|
+
</Can>
|
|
311
|
+
|
|
312
|
+
// Show fallback instead of hiding
|
|
313
|
+
<Can permission="users.create" fallback={<CreateButton disabled />}>
|
|
314
|
+
<CreateButton />
|
|
315
|
+
</Can>
|
|
316
|
+
|
|
317
|
+
// Role-based
|
|
318
|
+
<Can role="admin">
|
|
319
|
+
<AdminPanel />
|
|
320
|
+
</Can>
|
|
321
|
+
|
|
322
|
+
// Require all permissions
|
|
323
|
+
<Can permissions={["orders.view", "orders.export"]}>
|
|
324
|
+
<ExportButton />
|
|
325
|
+
</Can>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Resource-level RBAC
|
|
329
|
+
|
|
330
|
+
Protect entire resources (pages + sidebar items) via `meta`:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
{
|
|
334
|
+
name: "billing",
|
|
335
|
+
label: "Billing",
|
|
336
|
+
path: "/billing",
|
|
337
|
+
meta: {
|
|
338
|
+
requiredRoles: ["admin"], // must have at least one
|
|
339
|
+
requiredPermissions: ["billing.read"], // must have all
|
|
340
|
+
hideIfUnauthorized: true, // hide from sidebar (default: true)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
- When `hideIfUnauthorized: true` (default) — the item disappears from the sidebar
|
|
346
|
+
- Navigating directly to the path renders `<AccessDenied />`
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Components
|
|
351
|
+
|
|
352
|
+
### `<BackofficeApp>`
|
|
353
|
+
|
|
354
|
+
The root component. Wraps everything with auth context, guards, and layout.
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
<BackofficeApp
|
|
358
|
+
config={config}
|
|
359
|
+
authProvider={authProvider}
|
|
360
|
+
dataProvider={dataProvider} // optional
|
|
361
|
+
NavLink={NavLink} // optional, pass your router's Link
|
|
362
|
+
currentPath={pathname} // optional, for active sidebar item
|
|
363
|
+
loginPageProps={{ logo: "/logo.svg", appName: "My App" }}
|
|
364
|
+
headerActionLabel="New Record"
|
|
365
|
+
onHeaderAction={() => router.push("/new")}
|
|
366
|
+
onLoginSuccess={() => router.push("/dashboard")}
|
|
367
|
+
>
|
|
368
|
+
{children}
|
|
369
|
+
</BackofficeApp>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### `<AppShell>`
|
|
373
|
+
|
|
374
|
+
The layout shell used internally. Use it directly only when you need full control.
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
<AppShell
|
|
378
|
+
config={config}
|
|
379
|
+
NavLink={NavLink}
|
|
380
|
+
currentPath={pathname}
|
|
381
|
+
pageTitle="Users"
|
|
382
|
+
headerActionLabel="Invite"
|
|
383
|
+
onHeaderAction={handleInvite}
|
|
384
|
+
>
|
|
385
|
+
<UsersPage />
|
|
386
|
+
</AppShell>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### `<AuthGuard>`
|
|
390
|
+
|
|
391
|
+
Redirect unauthenticated users to the login page:
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
<AuthGuard renderLogin={() => <MyCustomLogin />}>
|
|
395
|
+
<ProtectedPage />
|
|
396
|
+
</AuthGuard>
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### `<ResourceGuard>`
|
|
400
|
+
|
|
401
|
+
Protect a page by resource meta:
|
|
402
|
+
|
|
403
|
+
```tsx
|
|
404
|
+
<ResourceGuard meta={{ requiredPermissions: ["users.read"] }}>
|
|
405
|
+
<UsersPage />
|
|
406
|
+
</ResourceGuard>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### `<AccessDenied>`
|
|
410
|
+
|
|
411
|
+
Default access-denied page. Shown automatically by `<ResourceGuard>`.
|
|
412
|
+
|
|
413
|
+
```tsx
|
|
414
|
+
<AccessDenied
|
|
415
|
+
message="You need the 'admin' role to access this page."
|
|
416
|
+
redirectPath="/dashboard"
|
|
417
|
+
redirectLabel="Go to Dashboard"
|
|
418
|
+
/>
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### `<LoadingScreen>`
|
|
422
|
+
|
|
423
|
+
Shown during the auth boot phase. Override via `renderLoading`:
|
|
424
|
+
|
|
425
|
+
```tsx
|
|
426
|
+
<BackofficeApp renderLoading={() => <MySpinner />} ... />
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Custom login page
|
|
432
|
+
|
|
433
|
+
```tsx
|
|
434
|
+
<BackofficeApp
|
|
435
|
+
renderLogin={() => <MyCustomLoginPage />}
|
|
436
|
+
...
|
|
437
|
+
/>
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Inside `MyCustomLoginPage`, use `useAuth()` to call `login()`:
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
function MyCustomLoginPage() {
|
|
444
|
+
const { login } = useAuth();
|
|
445
|
+
|
|
446
|
+
const handleSubmit = async (email: string, password: string) => {
|
|
447
|
+
await login(email, password);
|
|
448
|
+
};
|
|
449
|
+
// ...
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Type reference
|
|
456
|
+
|
|
457
|
+
### `User`
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
interface User {
|
|
461
|
+
id: string;
|
|
462
|
+
email: string;
|
|
463
|
+
name?: string;
|
|
464
|
+
roles?: string[];
|
|
465
|
+
permissions?: string[];
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### `Resource`
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
interface Resource {
|
|
473
|
+
name: string;
|
|
474
|
+
label: string;
|
|
475
|
+
path: string;
|
|
476
|
+
icon?: ComponentType<{ className?: string }>;
|
|
477
|
+
list?: ComponentType;
|
|
478
|
+
create?: ComponentType;
|
|
479
|
+
edit?: ComponentType;
|
|
480
|
+
show?: ComponentType;
|
|
481
|
+
meta?: ResourceMeta;
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### `ResourceMeta`
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
interface ResourceMeta {
|
|
489
|
+
requiredRoles?: string[]; // at least one role must match
|
|
490
|
+
requiredPermissions?: string[]; // all permissions must match
|
|
491
|
+
hideIfUnauthorized?: boolean; // default true
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### `BackofficeConfig`
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
interface BackofficeConfig {
|
|
499
|
+
appName: string;
|
|
500
|
+
logo?: ComponentType<{ className?: string }> | string;
|
|
501
|
+
resources?: Resource[];
|
|
502
|
+
sidebarGroups?: SidebarGroup[];
|
|
503
|
+
sidebarFooterLinks?: Array<{ label: string; path: string; icon?: ComponentType }>;
|
|
504
|
+
loginRedirect?: string;
|
|
505
|
+
logoutRedirect?: string;
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## Security note
|
|
512
|
+
|
|
513
|
+
> **The RBAC system in this framework is purely for UI/UX.** It hides and shows elements; it does not enforce security. Your backend API must validate every request independently.
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Exports
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
// Root
|
|
521
|
+
export { BackofficeApp } from "@specscreen/backoffice-core";
|
|
522
|
+
|
|
523
|
+
// Hooks
|
|
524
|
+
export { useAuth, usePermissions } from "@specscreen/backoffice-core";
|
|
525
|
+
|
|
526
|
+
// Guards
|
|
527
|
+
export { AuthGuard, ResourceGuard, Can } from "@specscreen/backoffice-core";
|
|
528
|
+
|
|
529
|
+
// Layout
|
|
530
|
+
export { AppShell, Sidebar, SidebarToggle } from "@specscreen/backoffice-core";
|
|
531
|
+
|
|
532
|
+
// Feedback
|
|
533
|
+
export { LoginPage, AccessDenied, LoadingScreen } from "@specscreen/backoffice-core";
|
|
534
|
+
|
|
535
|
+
// RBAC utilities (pure functions, no React)
|
|
536
|
+
export {
|
|
537
|
+
canAccessResource,
|
|
538
|
+
evaluateCan,
|
|
539
|
+
evaluateHasRole,
|
|
540
|
+
evaluateCanAny,
|
|
541
|
+
evaluateCanAll,
|
|
542
|
+
} from "@specscreen/backoffice-core";
|
|
543
|
+
|
|
544
|
+
// Types
|
|
545
|
+
export type {
|
|
546
|
+
User, AuthProvider, AuthState,
|
|
547
|
+
Resource, ResourceMeta, SidebarGroup,
|
|
548
|
+
BackofficeConfig, DataProvider,
|
|
549
|
+
} from "@specscreen/backoffice-core";
|
|
550
|
+
|
|
551
|
+
// Styles
|
|
552
|
+
import "@specscreen/backoffice-core/styles";
|
|
553
|
+
```
|