secure-role-guard 1.0.0 → 1.0.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 +616 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
- [API Reference](#api-reference)
|
|
22
22
|
- [Real-World Examples](#real-world-examples)
|
|
23
23
|
- [Framework Compatibility](#framework-compatibility)
|
|
24
|
+
- [Fixed Roles vs Dynamic Roles](#fixed-roles-vs-dynamic-roles)
|
|
24
25
|
- [Common Mistakes to Avoid](#common-mistakes-to-avoid)
|
|
25
26
|
- [License](#license)
|
|
26
27
|
|
|
@@ -610,6 +611,621 @@ if (tenantId) {
|
|
|
610
611
|
|
|
611
612
|
---
|
|
612
613
|
|
|
614
|
+
## Fixed Roles vs Dynamic Roles
|
|
615
|
+
|
|
616
|
+
This package supports both **Fixed (Hardcoded) Roles** and **Dynamic (Database-driven) Roles**. Choose the approach that fits your application.
|
|
617
|
+
|
|
618
|
+
### Architecture Overview
|
|
619
|
+
|
|
620
|
+
```
|
|
621
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
622
|
+
│ YOUR APPLICATION │
|
|
623
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
624
|
+
│ │
|
|
625
|
+
│ OPTION A: Fixed Roles OPTION B: Dynamic Roles │
|
|
626
|
+
│ ┌───────────────────┐ ┌───────────────────┐ │
|
|
627
|
+
│ │ roles.ts file │ │ Database │ │
|
|
628
|
+
│ │ (hardcoded) │ │ (MongoDB/PG/SQL) │ │
|
|
629
|
+
│ └─────────┬─────────┘ └─────────┬─────────┘ │
|
|
630
|
+
│ │ │ │
|
|
631
|
+
│ ▼ ▼ │
|
|
632
|
+
│ ┌───────────────────┐ ┌───────────────────┐ │
|
|
633
|
+
│ │ defineRoles() │ │ loadRolesFromDB()│ │
|
|
634
|
+
│ │ (at build time) │ │ (at runtime) │ │
|
|
635
|
+
│ └─────────┬─────────┘ └─────────┬─────────┘ │
|
|
636
|
+
│ │ │ │
|
|
637
|
+
│ └──────────────┬───────────────────┘ │
|
|
638
|
+
│ ▼ │
|
|
639
|
+
│ ┌───────────────────────────────────┐ │
|
|
640
|
+
│ │ secure-role-guard │ │
|
|
641
|
+
│ │ (same API for both approaches) │ │
|
|
642
|
+
│ └───────────────────────────────────┘ │
|
|
643
|
+
│ │
|
|
644
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
### Approach 1: Fixed Roles (Hardcoded)
|
|
650
|
+
|
|
651
|
+
Best for applications with **predefined, unchanging roles**.
|
|
652
|
+
|
|
653
|
+
#### When to Use Fixed Roles
|
|
654
|
+
|
|
655
|
+
- ✅ Small to medium applications
|
|
656
|
+
- ✅ Roles rarely change
|
|
657
|
+
- ✅ Simple admin/user/viewer hierarchy
|
|
658
|
+
- ✅ You want faster startup (no DB query needed)
|
|
659
|
+
|
|
660
|
+
#### Frontend Example (React/Next.js)
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
// lib/roles.ts - Define roles at build time
|
|
664
|
+
import { defineRoles } from "secure-role-guard";
|
|
665
|
+
|
|
666
|
+
export const roleRegistry = defineRoles({
|
|
667
|
+
superadmin: ["*"], // Full access
|
|
668
|
+
admin: ["user.read", "user.create", "user.update", "user.delete", "report.*"],
|
|
669
|
+
manager: ["user.read", "user.update", "report.view"],
|
|
670
|
+
support: ["ticket.read", "ticket.reply", "user.read"],
|
|
671
|
+
viewer: ["user.read", "report.view"],
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// -------------------------------------------------------
|
|
675
|
+
// app/providers.tsx - Setup Provider
|
|
676
|
+
("use client");
|
|
677
|
+
|
|
678
|
+
import { PermissionProvider } from "secure-role-guard/react";
|
|
679
|
+
import { roleRegistry } from "@/lib/roles";
|
|
680
|
+
|
|
681
|
+
interface User {
|
|
682
|
+
id: string;
|
|
683
|
+
roles: string[];
|
|
684
|
+
permissions?: string[];
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export function AuthProvider({
|
|
688
|
+
children,
|
|
689
|
+
user,
|
|
690
|
+
}: {
|
|
691
|
+
children: React.ReactNode;
|
|
692
|
+
user: User | null;
|
|
693
|
+
}) {
|
|
694
|
+
return (
|
|
695
|
+
<PermissionProvider user={user} registry={roleRegistry}>
|
|
696
|
+
{children}
|
|
697
|
+
</PermissionProvider>
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// -------------------------------------------------------
|
|
702
|
+
// components/Dashboard.tsx - Use Permissions
|
|
703
|
+
import { Can, useCan } from "secure-role-guard/react";
|
|
704
|
+
|
|
705
|
+
export function Dashboard() {
|
|
706
|
+
const canManageUsers = useCan("user.update");
|
|
707
|
+
|
|
708
|
+
return (
|
|
709
|
+
<div>
|
|
710
|
+
<h1>Dashboard</h1>
|
|
711
|
+
|
|
712
|
+
{/* Declarative approach */}
|
|
713
|
+
<Can permission="user.create">
|
|
714
|
+
<button>Add New User</button>
|
|
715
|
+
</Can>
|
|
716
|
+
|
|
717
|
+
<Can permission="report.view">
|
|
718
|
+
<ReportsSection />
|
|
719
|
+
</Can>
|
|
720
|
+
|
|
721
|
+
<Can permissions={["user.delete", "user.update"]} anyOf>
|
|
722
|
+
<UserManagement />
|
|
723
|
+
</Can>
|
|
724
|
+
|
|
725
|
+
{/* Programmatic approach */}
|
|
726
|
+
{canManageUsers && <EditUserButton />}
|
|
727
|
+
</div>
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
#### Backend Example (Express/Fastify)
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
// server.ts - Express with Fixed Roles
|
|
736
|
+
import express from "express";
|
|
737
|
+
import { defineRoles, canUser } from "secure-role-guard/core";
|
|
738
|
+
import { requirePermission } from "secure-role-guard/adapters/express";
|
|
739
|
+
|
|
740
|
+
const app = express();
|
|
741
|
+
|
|
742
|
+
// Same role definitions as frontend
|
|
743
|
+
const roleRegistry = defineRoles({
|
|
744
|
+
superadmin: ["*"],
|
|
745
|
+
admin: ["user.read", "user.create", "user.update", "user.delete"],
|
|
746
|
+
manager: ["user.read", "user.update"],
|
|
747
|
+
viewer: ["user.read"],
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// YOUR auth middleware (this package doesn't do auth)
|
|
751
|
+
app.use(yourAuthMiddleware); // Sets req.user
|
|
752
|
+
|
|
753
|
+
// Protected routes with middleware
|
|
754
|
+
app.get(
|
|
755
|
+
"/api/users",
|
|
756
|
+
requirePermission("user.read", roleRegistry),
|
|
757
|
+
async (req, res) => {
|
|
758
|
+
const users = await db.users.findAll();
|
|
759
|
+
res.json(users);
|
|
760
|
+
}
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
app.post(
|
|
764
|
+
"/api/users",
|
|
765
|
+
requirePermission("user.create", roleRegistry),
|
|
766
|
+
async (req, res) => {
|
|
767
|
+
const user = await db.users.create(req.body);
|
|
768
|
+
res.json(user);
|
|
769
|
+
}
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
// Manual permission check (for complex logic)
|
|
773
|
+
app.put("/api/users/:id", async (req, res) => {
|
|
774
|
+
const user = req.user;
|
|
775
|
+
|
|
776
|
+
if (!canUser(user, "user.update", roleRegistry)) {
|
|
777
|
+
return res.status(403).json({ error: "Forbidden" });
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Additional business logic
|
|
781
|
+
const targetUser = await db.users.findById(req.params.id);
|
|
782
|
+
|
|
783
|
+
// Example: Managers can only edit non-admin users
|
|
784
|
+
if (
|
|
785
|
+
targetUser.roles.includes("admin") &&
|
|
786
|
+
!canUser(user, "admin.manage", roleRegistry)
|
|
787
|
+
) {
|
|
788
|
+
return res.status(403).json({ error: "Cannot edit admin users" });
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const updated = await db.users.update(req.params.id, req.body);
|
|
792
|
+
res.json(updated);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
app.listen(3000);
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
### Approach 2: Dynamic Roles (Database-Driven)
|
|
801
|
+
|
|
802
|
+
Best for applications where **admin can create/modify roles at runtime**.
|
|
803
|
+
|
|
804
|
+
#### When to Use Dynamic Roles
|
|
805
|
+
|
|
806
|
+
- ✅ Enterprise SaaS applications
|
|
807
|
+
- ✅ Admin should create custom roles (e.g., "HR Manager", "Finance Lead")
|
|
808
|
+
- ✅ Roles change frequently
|
|
809
|
+
- ✅ Multi-tenant with different roles per tenant
|
|
810
|
+
|
|
811
|
+
#### Database Schema Examples
|
|
812
|
+
|
|
813
|
+
**MongoDB:**
|
|
814
|
+
|
|
815
|
+
```javascript
|
|
816
|
+
// roles collection
|
|
817
|
+
{
|
|
818
|
+
_id: ObjectId("..."),
|
|
819
|
+
name: "hr_manager",
|
|
820
|
+
display_name: "HR Manager",
|
|
821
|
+
permissions: ["employee.read", "employee.create", "employee.update", "leave.approve"],
|
|
822
|
+
is_active: true,
|
|
823
|
+
tenant_id: ObjectId("..."), // For multi-tenant
|
|
824
|
+
created_at: ISODate("...")
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// users collection
|
|
828
|
+
{
|
|
829
|
+
_id: ObjectId("..."),
|
|
830
|
+
email: "john@example.com",
|
|
831
|
+
roles: [ObjectId("role1"), ObjectId("role2")],
|
|
832
|
+
direct_permissions: ["special.feature"], // User-specific permissions
|
|
833
|
+
tenant_id: ObjectId("...")
|
|
834
|
+
}
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
**PostgreSQL:**
|
|
838
|
+
|
|
839
|
+
```sql
|
|
840
|
+
-- roles table
|
|
841
|
+
CREATE TABLE roles (
|
|
842
|
+
id SERIAL PRIMARY KEY,
|
|
843
|
+
name VARCHAR(50) UNIQUE NOT NULL,
|
|
844
|
+
display_name VARCHAR(100),
|
|
845
|
+
is_active BOOLEAN DEFAULT true,
|
|
846
|
+
tenant_id INTEGER REFERENCES tenants(id),
|
|
847
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
-- permissions table
|
|
851
|
+
CREATE TABLE permissions (
|
|
852
|
+
id SERIAL PRIMARY KEY,
|
|
853
|
+
code VARCHAR(100) UNIQUE NOT NULL, -- e.g., 'user.read'
|
|
854
|
+
description TEXT
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
-- role_permissions (many-to-many)
|
|
858
|
+
CREATE TABLE role_permissions (
|
|
859
|
+
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
|
|
860
|
+
permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE,
|
|
861
|
+
PRIMARY KEY (role_id, permission_id)
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
-- user_roles (many-to-many)
|
|
865
|
+
CREATE TABLE user_roles (
|
|
866
|
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
867
|
+
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
|
|
868
|
+
PRIMARY KEY (user_id, role_id)
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
-- user_permissions (direct permissions, bypass roles)
|
|
872
|
+
CREATE TABLE user_permissions (
|
|
873
|
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
874
|
+
permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE,
|
|
875
|
+
PRIMARY KEY (user_id, permission_id)
|
|
876
|
+
);
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
**MySQL:**
|
|
880
|
+
|
|
881
|
+
```sql
|
|
882
|
+
-- Similar to PostgreSQL, with MySQL syntax
|
|
883
|
+
CREATE TABLE roles (
|
|
884
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
885
|
+
name VARCHAR(50) UNIQUE NOT NULL,
|
|
886
|
+
display_name VARCHAR(100),
|
|
887
|
+
is_active TINYINT(1) DEFAULT 1,
|
|
888
|
+
tenant_id INT,
|
|
889
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
CREATE TABLE permissions (
|
|
893
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
894
|
+
code VARCHAR(100) UNIQUE NOT NULL,
|
|
895
|
+
description TEXT
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
CREATE TABLE role_permissions (
|
|
899
|
+
role_id INT,
|
|
900
|
+
permission_id INT,
|
|
901
|
+
PRIMARY KEY (role_id, permission_id),
|
|
902
|
+
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
|
903
|
+
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
|
904
|
+
);
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
#### Backend: Loading Dynamic Roles
|
|
908
|
+
|
|
909
|
+
```typescript
|
|
910
|
+
// lib/dynamic-roles.ts
|
|
911
|
+
import { defineRoles, RoleRegistry } from "secure-role-guard/core";
|
|
912
|
+
|
|
913
|
+
// Interface for database abstraction
|
|
914
|
+
interface RoleFromDB {
|
|
915
|
+
name: string;
|
|
916
|
+
permissions: string[];
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
interface IRoleRepository {
|
|
920
|
+
getAllActiveRoles(): Promise<RoleFromDB[]>;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// ============================================================
|
|
924
|
+
// MongoDB Implementation
|
|
925
|
+
// ============================================================
|
|
926
|
+
class MongoRoleRepository implements IRoleRepository {
|
|
927
|
+
async getAllActiveRoles(): Promise<RoleFromDB[]> {
|
|
928
|
+
const roles = await RoleModel.find({ is_active: true })
|
|
929
|
+
.populate("permissions")
|
|
930
|
+
.lean();
|
|
931
|
+
|
|
932
|
+
return roles.map((role) => ({
|
|
933
|
+
name: role.name,
|
|
934
|
+
permissions: role.permissions.map((p: any) => p.code),
|
|
935
|
+
}));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// ============================================================
|
|
940
|
+
// PostgreSQL Implementation (using Prisma)
|
|
941
|
+
// ============================================================
|
|
942
|
+
class PostgresRoleRepository implements IRoleRepository {
|
|
943
|
+
async getAllActiveRoles(): Promise<RoleFromDB[]> {
|
|
944
|
+
const roles = await prisma.role.findMany({
|
|
945
|
+
where: { is_active: true },
|
|
946
|
+
include: {
|
|
947
|
+
role_permissions: {
|
|
948
|
+
include: { permission: true },
|
|
949
|
+
},
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
return roles.map((role) => ({
|
|
954
|
+
name: role.name,
|
|
955
|
+
permissions: role.role_permissions.map((rp) => rp.permission.code),
|
|
956
|
+
}));
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ============================================================
|
|
961
|
+
// MySQL Implementation (using mysql2)
|
|
962
|
+
// ============================================================
|
|
963
|
+
class MySQLRoleRepository implements IRoleRepository {
|
|
964
|
+
async getAllActiveRoles(): Promise<RoleFromDB[]> {
|
|
965
|
+
const [rows] = await pool.query(`
|
|
966
|
+
SELECT r.name, GROUP_CONCAT(p.code) as permissions
|
|
967
|
+
FROM roles r
|
|
968
|
+
LEFT JOIN role_permissions rp ON r.id = rp.role_id
|
|
969
|
+
LEFT JOIN permissions p ON rp.permission_id = p.id
|
|
970
|
+
WHERE r.is_active = 1
|
|
971
|
+
GROUP BY r.id, r.name
|
|
972
|
+
`);
|
|
973
|
+
|
|
974
|
+
return (rows as any[]).map((row) => ({
|
|
975
|
+
name: row.name,
|
|
976
|
+
permissions: row.permissions ? row.permissions.split(",") : [],
|
|
977
|
+
}));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// ============================================================
|
|
982
|
+
// Dynamic Role Registry Factory
|
|
983
|
+
// ============================================================
|
|
984
|
+
let cachedRegistry: RoleRegistry | null = null;
|
|
985
|
+
let cacheExpiry = 0;
|
|
986
|
+
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
987
|
+
|
|
988
|
+
export async function getDynamicRoleRegistry(
|
|
989
|
+
repository: IRoleRepository
|
|
990
|
+
): Promise<RoleRegistry> {
|
|
991
|
+
const now = Date.now();
|
|
992
|
+
|
|
993
|
+
// Return cached if valid
|
|
994
|
+
if (cachedRegistry && now < cacheExpiry) {
|
|
995
|
+
return cachedRegistry;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Fetch from database
|
|
999
|
+
const rolesFromDB = await repository.getAllActiveRoles();
|
|
1000
|
+
|
|
1001
|
+
// Transform to RoleDefinition format
|
|
1002
|
+
const roleDefinition: Record<string, readonly string[]> = {};
|
|
1003
|
+
for (const role of rolesFromDB) {
|
|
1004
|
+
roleDefinition[role.name] = role.permissions;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Create registry
|
|
1008
|
+
cachedRegistry = defineRoles(roleDefinition);
|
|
1009
|
+
cacheExpiry = now + CACHE_TTL;
|
|
1010
|
+
|
|
1011
|
+
return cachedRegistry;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Force refresh (call when admin updates roles)
|
|
1015
|
+
export function invalidateRoleCache(): void {
|
|
1016
|
+
cachedRegistry = null;
|
|
1017
|
+
cacheExpiry = 0;
|
|
1018
|
+
}
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
#### Backend: Using Dynamic Roles in Express
|
|
1022
|
+
|
|
1023
|
+
```typescript
|
|
1024
|
+
// server.ts
|
|
1025
|
+
import express from "express";
|
|
1026
|
+
import { canUser } from "secure-role-guard/core";
|
|
1027
|
+
import {
|
|
1028
|
+
getDynamicRoleRegistry,
|
|
1029
|
+
invalidateRoleCache,
|
|
1030
|
+
} from "./lib/dynamic-roles";
|
|
1031
|
+
|
|
1032
|
+
const app = express();
|
|
1033
|
+
const roleRepository = new MongoRoleRepository(); // or PostgresRoleRepository
|
|
1034
|
+
|
|
1035
|
+
// Middleware to attach registry to request
|
|
1036
|
+
app.use(async (req, res, next) => {
|
|
1037
|
+
try {
|
|
1038
|
+
req.roleRegistry = await getDynamicRoleRegistry(roleRepository);
|
|
1039
|
+
next();
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
console.error("Failed to load roles:", error);
|
|
1042
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
// Protected routes using dynamic roles
|
|
1047
|
+
app.get("/api/employees", async (req, res) => {
|
|
1048
|
+
if (!canUser(req.user, "employee.read", req.roleRegistry)) {
|
|
1049
|
+
return res.status(403).json({ error: "Forbidden" });
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const employees = await db.employees.findAll();
|
|
1053
|
+
res.json(employees);
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
// Admin creates a new role
|
|
1057
|
+
app.post("/api/admin/roles", async (req, res) => {
|
|
1058
|
+
if (!canUser(req.user, "role.create", req.roleRegistry)) {
|
|
1059
|
+
return res.status(403).json({ error: "Forbidden" });
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const { name, permissions } = req.body;
|
|
1063
|
+
|
|
1064
|
+
// Save to database
|
|
1065
|
+
await RoleModel.create({ name, permissions, is_active: true });
|
|
1066
|
+
|
|
1067
|
+
// Invalidate cache so new role is available
|
|
1068
|
+
invalidateRoleCache();
|
|
1069
|
+
|
|
1070
|
+
res.json({ success: true });
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
app.listen(3000);
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
#### Frontend: Using Dynamic Roles
|
|
1077
|
+
|
|
1078
|
+
```typescript
|
|
1079
|
+
// lib/auth-context.tsx
|
|
1080
|
+
"use client";
|
|
1081
|
+
|
|
1082
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
1083
|
+
import {
|
|
1084
|
+
PermissionProvider,
|
|
1085
|
+
RoleRegistry,
|
|
1086
|
+
defineRoles,
|
|
1087
|
+
} from "secure-role-guard/react";
|
|
1088
|
+
|
|
1089
|
+
interface User {
|
|
1090
|
+
id: string;
|
|
1091
|
+
email: string;
|
|
1092
|
+
roles: string[];
|
|
1093
|
+
permissions: string[];
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
interface AuthContextValue {
|
|
1097
|
+
user: User | null;
|
|
1098
|
+
isLoading: boolean;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const AuthContext = createContext<AuthContextValue>({
|
|
1102
|
+
user: null,
|
|
1103
|
+
isLoading: true,
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
export function DynamicAuthProvider({
|
|
1107
|
+
children,
|
|
1108
|
+
}: {
|
|
1109
|
+
children: React.ReactNode;
|
|
1110
|
+
}) {
|
|
1111
|
+
const [user, setUser] = useState<User | null>(null);
|
|
1112
|
+
const [roleRegistry, setRoleRegistry] = useState<RoleRegistry | null>(null);
|
|
1113
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
1114
|
+
|
|
1115
|
+
useEffect(() => {
|
|
1116
|
+
async function loadUserAndRoles() {
|
|
1117
|
+
try {
|
|
1118
|
+
// Fetch current user
|
|
1119
|
+
const userRes = await fetch("/api/auth/me");
|
|
1120
|
+
const userData = await userRes.json();
|
|
1121
|
+
|
|
1122
|
+
if (!userData.user) {
|
|
1123
|
+
setIsLoading(false);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Fetch dynamic roles from backend
|
|
1128
|
+
const rolesRes = await fetch("/api/auth/roles");
|
|
1129
|
+
const rolesData = await rolesRes.json();
|
|
1130
|
+
|
|
1131
|
+
// Create registry from dynamic roles
|
|
1132
|
+
// rolesData format: { admin: ['user.read', ...], manager: [...] }
|
|
1133
|
+
const registry = defineRoles(rolesData.roles);
|
|
1134
|
+
|
|
1135
|
+
setUser(userData.user);
|
|
1136
|
+
setRoleRegistry(registry);
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
console.error("Failed to load auth:", error);
|
|
1139
|
+
} finally {
|
|
1140
|
+
setIsLoading(false);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
loadUserAndRoles();
|
|
1145
|
+
}, []);
|
|
1146
|
+
|
|
1147
|
+
if (isLoading) {
|
|
1148
|
+
return <div>Loading...</div>;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (!roleRegistry) {
|
|
1152
|
+
return <div>Failed to load permissions</div>;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return (
|
|
1156
|
+
<AuthContext.Provider value={{ user, isLoading }}>
|
|
1157
|
+
<PermissionProvider user={user} registry={roleRegistry}>
|
|
1158
|
+
{children}
|
|
1159
|
+
</PermissionProvider>
|
|
1160
|
+
</AuthContext.Provider>
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// -------------------------------------------------------
|
|
1165
|
+
// API Route: Return roles for frontend
|
|
1166
|
+
// app/api/auth/roles/route.ts
|
|
1167
|
+
|
|
1168
|
+
import { NextResponse } from "next/server";
|
|
1169
|
+
|
|
1170
|
+
export async function GET() {
|
|
1171
|
+
// Fetch roles from database
|
|
1172
|
+
const roles = await RoleModel.find({ is_active: true }).lean();
|
|
1173
|
+
|
|
1174
|
+
// Transform to { roleName: permissions[] } format
|
|
1175
|
+
const roleMap: Record<string, string[]> = {};
|
|
1176
|
+
for (const role of roles) {
|
|
1177
|
+
roleMap[role.name] = role.permissions;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
return NextResponse.json({ roles: roleMap });
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// -------------------------------------------------------
|
|
1184
|
+
// Usage in Components (same as fixed roles!)
|
|
1185
|
+
|
|
1186
|
+
import { Can, useCan } from "secure-role-guard/react";
|
|
1187
|
+
|
|
1188
|
+
function EmployeeDashboard() {
|
|
1189
|
+
const canApproveLeave = useCan("leave.approve");
|
|
1190
|
+
|
|
1191
|
+
return (
|
|
1192
|
+
<div>
|
|
1193
|
+
<Can permission="employee.read">
|
|
1194
|
+
<EmployeeList />
|
|
1195
|
+
</Can>
|
|
1196
|
+
|
|
1197
|
+
<Can permission="employee.create">
|
|
1198
|
+
<AddEmployeeButton />
|
|
1199
|
+
</Can>
|
|
1200
|
+
|
|
1201
|
+
{canApproveLeave && <LeaveApprovalQueue />}
|
|
1202
|
+
</div>
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
---
|
|
1208
|
+
|
|
1209
|
+
### Comparison: Fixed vs Dynamic
|
|
1210
|
+
|
|
1211
|
+
| Feature | Fixed Roles | Dynamic Roles |
|
|
1212
|
+
| ----------------------- | --------------------- | ---------------------------- |
|
|
1213
|
+
| **Setup Complexity** | Simple | More complex |
|
|
1214
|
+
| **Runtime Performance** | Faster (no DB query) | Slight overhead (cached) |
|
|
1215
|
+
| **Flexibility** | Limited | Full flexibility |
|
|
1216
|
+
| **Admin Control** | Code changes required | UI-based role management |
|
|
1217
|
+
| **Use Case** | Simple apps, MVPs | Enterprise, SaaS |
|
|
1218
|
+
| **Role Changes** | Deploy required | Instant (cache invalidation) |
|
|
1219
|
+
|
|
1220
|
+
### Key Points
|
|
1221
|
+
|
|
1222
|
+
1. **Package is database-agnostic** - You fetch data, we check permissions
|
|
1223
|
+
2. **Same API for both approaches** - `canUser()`, `<Can>`, `useCan()` work identically
|
|
1224
|
+
3. **Frontend mirrors backend** - Keep role definitions in sync
|
|
1225
|
+
4. **Always validate on backend** - Frontend is for UX, backend is for security
|
|
1226
|
+
|
|
1227
|
+
---
|
|
1228
|
+
|
|
613
1229
|
## Common Mistakes to Avoid
|
|
614
1230
|
|
|
615
1231
|
### ❌ DON'T: Parse JWT in this package
|