@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 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
+ ```