@sunboyoo/supabase-rbac 1.0.2

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.
@@ -0,0 +1,116 @@
1
+ type UUID = string;
2
+ type RoleCheckMode = "any" | "all";
3
+ type RoleIdsByApp = Record<string, UUID[]>;
4
+ interface RBACClaims {
5
+ app_metadata?: {
6
+ role_ids?: RoleIdsByApp;
7
+ [k: string]: unknown;
8
+ };
9
+ sub?: string;
10
+ exp?: number;
11
+ [k: string]: unknown;
12
+ }
13
+ interface Logger {
14
+ debug?: (msg: string, meta?: unknown) => void;
15
+ info?: (msg: string, meta?: unknown) => void;
16
+ warn?: (msg: string, meta?: unknown) => void;
17
+ error?: (msg: string, meta?: unknown) => void;
18
+ }
19
+ /** EN: Client permission-name support is optional and should be feature-flagged.
20
+ * 中文:permission-name 能力是可选增强,建议 feature-flag 控制,默认关闭以最小暴露面。
21
+ */
22
+ interface PermissionNameOptions {
23
+ /** EN: Enable permission-name list loading via RPC. Default: false
24
+ * 中文:是否启用通过 RPC 加载 permission-name 列表。默认 false
25
+ */
26
+ enabled?: boolean;
27
+ /** EN: RPC function name. Default: 'rbac.get_my_permissions' or 'get_my_permissions' depending on your PostgREST exposure.
28
+ * Best practice: expose via RPC name 'get_my_permissions' (no schema prefix) if you keep rbac schema not exposed.
29
+ * 中文:RPC 函数名。默认 'get_my_permissions'(推荐:不带 schema 前缀,更利于隐藏 schema)
30
+ */
31
+ rpcName?: string;
32
+ /** EN: TTL for permission list cache in ms. Default: 60_000
33
+ * 中文:permission 列表缓存 TTL(毫秒)。默认 60秒
34
+ */
35
+ ttlMs?: number;
36
+ /** EN: Non-blocking: roleIds update first, permissions load in background. Default: true
37
+ * 中文:非阻塞:先更新 roleIds,permissions 后台加载。默认 true
38
+ */
39
+ nonBlocking?: boolean;
40
+ }
41
+ interface RBACProviderProps {
42
+ /** EN: Supabase client instance
43
+ * 中文:Supabase client 实例
44
+ */
45
+ client: any;
46
+ /** EN: current app_id
47
+ * 中文:当前 app_id
48
+ */
49
+ appId: string;
50
+ /** EN: include global roles automatically (default true)
51
+ * 中文:自动合并 global 角色(默认 true)
52
+ */
53
+ includeGlobal?: boolean;
54
+ /** EN: global app id key (default 'global')
55
+ * 中文:global app_id key(默认 'global')
56
+ */
57
+ globalAppId?: string;
58
+ /** EN: Permission-name support options (optional)
59
+ * 中文:permission-name 可选增强配置
60
+ */
61
+ permissions?: PermissionNameOptions;
62
+ /** EN: Optional logger hook (no console spam by default)
63
+ * 中文:可选 logger(默认不输出 console)
64
+ */
65
+ logger?: Logger;
66
+ children: any;
67
+ }
68
+ interface RBACState {
69
+ appId: string;
70
+ /** EN: Auth state
71
+ * 中文:认证态(是否登录)
72
+ */
73
+ isAuthenticated: boolean;
74
+ /** EN: Parsed user id (best-effort)
75
+ * 中文:user id(尽力获取)
76
+ */
77
+ userId: UUID | null;
78
+ /** EN: Merged role ids for (appId + optional global)
79
+ * 中文:合并后的 roleIds(appId + 可选 global)
80
+ */
81
+ roleIds: UUID[];
82
+ /** EN: Optional permission-name list (only when enabled)
83
+ * 中文:可选的 permission-name 列表(仅启用时)
84
+ */
85
+ permissionNames: string[];
86
+ /** EN: Loading state for RBAC info (roles always; permissions optional)
87
+ * 中文:RBAC 加载状态(roles 必有;permissions 可选)
88
+ */
89
+ isRBACLoading: boolean;
90
+ /** EN: Permission-name loading state (only meaningful when enabled)
91
+ * 中文:permission-name 加载状态(启用时才有意义)
92
+ */
93
+ isPermissionLoading: boolean;
94
+ /** EN: Whether RBAC base (roles) is ready
95
+ * 中文:RBAC 基础(roles)是否就绪
96
+ */
97
+ isRBACReady: boolean;
98
+ /** EN: Whether permission list is ready (when enabled)
99
+ * 中文:permission 列表是否就绪(启用时)
100
+ */
101
+ isPermissionReady: boolean;
102
+ /** EN: Refresh RBAC state. forceSessionUpdate triggers supabase.auth.refreshSession()
103
+ * 中文:刷新 RBAC 状态。forceSessionUpdate 会触发 refreshSession 以重新注入 claims
104
+ */
105
+ refresh: (forceSessionUpdate?: boolean) => Promise<void>;
106
+ /** EN: Role gating (fast path)
107
+ * 中文:基于 roleIds 的 UI 判定(快路径)
108
+ */
109
+ hasRole: (requiredRoleIds: UUID[], mode?: RoleCheckMode) => boolean;
110
+ /** EN: Permission gating (slow path; optional)
111
+ * 中文:基于 permission-name 的 UI 判定(慢路径;可选)
112
+ */
113
+ hasPermission: (permissionName: string) => boolean;
114
+ }
115
+
116
+ export type { Logger as L, PermissionNameOptions as P, RoleCheckMode as R, UUID as U, RoleIdsByApp as a, RBACClaims as b, RBACProviderProps as c, RBACState as d };
@@ -0,0 +1,116 @@
1
+ type UUID = string;
2
+ type RoleCheckMode = "any" | "all";
3
+ type RoleIdsByApp = Record<string, UUID[]>;
4
+ interface RBACClaims {
5
+ app_metadata?: {
6
+ role_ids?: RoleIdsByApp;
7
+ [k: string]: unknown;
8
+ };
9
+ sub?: string;
10
+ exp?: number;
11
+ [k: string]: unknown;
12
+ }
13
+ interface Logger {
14
+ debug?: (msg: string, meta?: unknown) => void;
15
+ info?: (msg: string, meta?: unknown) => void;
16
+ warn?: (msg: string, meta?: unknown) => void;
17
+ error?: (msg: string, meta?: unknown) => void;
18
+ }
19
+ /** EN: Client permission-name support is optional and should be feature-flagged.
20
+ * 中文:permission-name 能力是可选增强,建议 feature-flag 控制,默认关闭以最小暴露面。
21
+ */
22
+ interface PermissionNameOptions {
23
+ /** EN: Enable permission-name list loading via RPC. Default: false
24
+ * 中文:是否启用通过 RPC 加载 permission-name 列表。默认 false
25
+ */
26
+ enabled?: boolean;
27
+ /** EN: RPC function name. Default: 'rbac.get_my_permissions' or 'get_my_permissions' depending on your PostgREST exposure.
28
+ * Best practice: expose via RPC name 'get_my_permissions' (no schema prefix) if you keep rbac schema not exposed.
29
+ * 中文:RPC 函数名。默认 'get_my_permissions'(推荐:不带 schema 前缀,更利于隐藏 schema)
30
+ */
31
+ rpcName?: string;
32
+ /** EN: TTL for permission list cache in ms. Default: 60_000
33
+ * 中文:permission 列表缓存 TTL(毫秒)。默认 60秒
34
+ */
35
+ ttlMs?: number;
36
+ /** EN: Non-blocking: roleIds update first, permissions load in background. Default: true
37
+ * 中文:非阻塞:先更新 roleIds,permissions 后台加载。默认 true
38
+ */
39
+ nonBlocking?: boolean;
40
+ }
41
+ interface RBACProviderProps {
42
+ /** EN: Supabase client instance
43
+ * 中文:Supabase client 实例
44
+ */
45
+ client: any;
46
+ /** EN: current app_id
47
+ * 中文:当前 app_id
48
+ */
49
+ appId: string;
50
+ /** EN: include global roles automatically (default true)
51
+ * 中文:自动合并 global 角色(默认 true)
52
+ */
53
+ includeGlobal?: boolean;
54
+ /** EN: global app id key (default 'global')
55
+ * 中文:global app_id key(默认 'global')
56
+ */
57
+ globalAppId?: string;
58
+ /** EN: Permission-name support options (optional)
59
+ * 中文:permission-name 可选增强配置
60
+ */
61
+ permissions?: PermissionNameOptions;
62
+ /** EN: Optional logger hook (no console spam by default)
63
+ * 中文:可选 logger(默认不输出 console)
64
+ */
65
+ logger?: Logger;
66
+ children: any;
67
+ }
68
+ interface RBACState {
69
+ appId: string;
70
+ /** EN: Auth state
71
+ * 中文:认证态(是否登录)
72
+ */
73
+ isAuthenticated: boolean;
74
+ /** EN: Parsed user id (best-effort)
75
+ * 中文:user id(尽力获取)
76
+ */
77
+ userId: UUID | null;
78
+ /** EN: Merged role ids for (appId + optional global)
79
+ * 中文:合并后的 roleIds(appId + 可选 global)
80
+ */
81
+ roleIds: UUID[];
82
+ /** EN: Optional permission-name list (only when enabled)
83
+ * 中文:可选的 permission-name 列表(仅启用时)
84
+ */
85
+ permissionNames: string[];
86
+ /** EN: Loading state for RBAC info (roles always; permissions optional)
87
+ * 中文:RBAC 加载状态(roles 必有;permissions 可选)
88
+ */
89
+ isRBACLoading: boolean;
90
+ /** EN: Permission-name loading state (only meaningful when enabled)
91
+ * 中文:permission-name 加载状态(启用时才有意义)
92
+ */
93
+ isPermissionLoading: boolean;
94
+ /** EN: Whether RBAC base (roles) is ready
95
+ * 中文:RBAC 基础(roles)是否就绪
96
+ */
97
+ isRBACReady: boolean;
98
+ /** EN: Whether permission list is ready (when enabled)
99
+ * 中文:permission 列表是否就绪(启用时)
100
+ */
101
+ isPermissionReady: boolean;
102
+ /** EN: Refresh RBAC state. forceSessionUpdate triggers supabase.auth.refreshSession()
103
+ * 中文:刷新 RBAC 状态。forceSessionUpdate 会触发 refreshSession 以重新注入 claims
104
+ */
105
+ refresh: (forceSessionUpdate?: boolean) => Promise<void>;
106
+ /** EN: Role gating (fast path)
107
+ * 中文:基于 roleIds 的 UI 判定(快路径)
108
+ */
109
+ hasRole: (requiredRoleIds: UUID[], mode?: RoleCheckMode) => boolean;
110
+ /** EN: Permission gating (slow path; optional)
111
+ * 中文:基于 permission-name 的 UI 判定(慢路径;可选)
112
+ */
113
+ hasPermission: (permissionName: string) => boolean;
114
+ }
115
+
116
+ export type { Logger as L, PermissionNameOptions as P, RoleCheckMode as R, UUID as U, RoleIdsByApp as a, RBACClaims as b, RBACProviderProps as c, RBACState as d };
@@ -0,0 +1,70 @@
1
+ /* =================================================================================================
2
+ ENGLISH COMMENT BLOCK (Architecture + Ops + Frontend Guidance)
3
+ ====================================================================================================
4
+
5
+ FILE PURPOSE
6
+ - This migration creates required extensions and the two dedicated schemas:
7
+ - rbac : all RBAC tables / views / core permission-check function
8
+ - hooks : Supabase Auth hook function used to inject claims into JWT
9
+
10
+ ARCHITECTURE OVERVIEW (High-level)
11
+ - Multi-App / Multi-Module RBAC in one Supabase project (SSO).
12
+ - RBAC is shared across apps; business tables remain app-private.
13
+ - Authorization is enforced in DB via RLS policies calling rbac.has_perm(app_id, perm_name).
14
+ - JWT stores role_ids (UUID) per app_id, injected by a custom access token hook.
15
+
16
+ IMPORTANT NOTES
17
+ - This file ONLY creates schemas and baseline schema revokes.
18
+ - Do NOT expose schemas rbac / hooks via Supabase Dashboard → API → Exposed Schemas.
19
+ - Stronger REVOKE/GRANT hardening happens in later migrations (do not duplicate here).
20
+
21
+ FRONTEND / EXTERNAL USAGE (Reference)
22
+ - Frontend typically does NOT call rbac.has_perm directly. RLS enforces permissions.
23
+ - After admin changes user roles, users must refresh token:
24
+ supabase.auth.refreshSession()
25
+
26
+ ==================================================================================================== */
27
+
28
+
29
+ /* =================================================================================================
30
+ 中文注释区块(架构 + 运维 + 前端指引)
31
+ ====================================================================================================
32
+
33
+ 文件目的
34
+ - 本迁移只负责创建必要扩展与两个 schema:
35
+ - rbac :RBAC 权限表/视图/核心判定函数
36
+ - hooks :Supabase Auth Hook(向 JWT 注入 claims)
37
+
38
+ 架构总览(高层)
39
+ - 同一 Supabase 项目下多 App/多模块(SSO)。
40
+ - RBAC 表全生态共用;业务表按 App 私有。
41
+ - 权限在数据库层用 RLS 强制执行,策略调用 rbac.has_perm(app_id, perm_name)。
42
+ - JWT 存 role_ids(UUID),通过 Auth access token hook 注入。
43
+
44
+ 重要说明
45
+ - 本文件只做 schema + 基础 revoke,不做完整授权收紧。
46
+ - Supabase Dashboard → API → Exposed Schemas 必须移除 rbac / hooks(强防枚举)。
47
+ - 更完整的 GRANT/REVOKE 会在后续迁移中集中处理,避免重复与漂移。
48
+
49
+ 前端/外部使用提示
50
+ - 前端通常不直接调用 rbac.has_perm,依赖 RLS 拦截并处理 401/403。
51
+ - 管理端更改用户角色后,用户需 refresh token:
52
+ supabase.auth.refreshSession()
53
+
54
+ ==================================================================================================== */
55
+
56
+
57
+ -- Create required extension(s) and schemas for RBAC / Hooks.
58
+ -- Note: Removing schemas from Supabase "Exposed Schemas" is a Dashboard setting, not SQL.
59
+
60
+ create extension if not exists pgcrypto;
61
+
62
+ create schema if not exists rbac;
63
+ create schema if not exists hooks;
64
+
65
+ -- Baseline schema lockdown (tables are locked down later via grants/revokes).
66
+ revoke all on schema rbac from public;
67
+ revoke all on schema hooks from public;
68
+
69
+ revoke all on schema rbac from anon, authenticated;
70
+ revoke all on schema hooks from anon, authenticated;
@@ -0,0 +1,134 @@
1
+ /* =================================================================================================
2
+ ENGLISH COMMENT BLOCK (Architecture + Ops + Frontend Guidance)
3
+ ====================================================================================================
4
+
5
+ FILE PURPOSE
6
+ - Creates RBAC core tables shared across all apps/modules:
7
+ - rbac.apps
8
+ - rbac.permissions
9
+ - rbac.roles
10
+ - rbac.role_permissions (with composite FK integrity)
11
+ - rbac.user_roles
12
+ - Creates required indexes for the authorization hot path.
13
+ - Disables RLS on RBAC tables (RBAC is protected via GRANT/REVOKE + schema isolation).
14
+
15
+ ARCHITECTURE KEYWORDS
16
+ - App-scoped Roles & Permissions (app_id)
17
+ - Global-Mirror Permissions (app_id='global', not NULL)
18
+ - Composite Foreign Key Consistency (prevents cross-app binding of roles/permissions)
19
+ - Shared RBAC, App-Private Data
20
+
21
+ GLOBAL-MIRROR SEMANTICS (Critical)
22
+ - 'global' is a real app_id row (never NULL).
23
+ - Global roles grant permissions ONLY via global permissions dictionary.
24
+ - If a global role should grant an app permission, you must create a mirrored permission:
25
+ rbac.permissions(app_id='global', name='<same permission name>').
26
+
27
+ OPERATIONS NOTES
28
+ - RBAC tables do NOT enable RLS to avoid FORCE RLS / policy drift operational risks.
29
+ - Security hardening and exposure control (revoke/grant) is applied in later migrations.
30
+
31
+ ==================================================================================================== */
32
+
33
+
34
+ /* =================================================================================================
35
+ 中文注释区块(架构 + 运维 + 前端指引)
36
+ ====================================================================================================
37
+
38
+ 文件目的
39
+ - 创建全生态共享的 RBAC 核心表:
40
+ - rbac.apps
41
+ - rbac.permissions
42
+ - rbac.roles
43
+ - rbac.role_permissions(复合外键强一致性)
44
+ - rbac.user_roles
45
+ - 创建鉴权热路径所需索引。
46
+ - RBAC 表不开 RLS(通过 GRANT/REVOKE + schema 隔离来保护)。
47
+
48
+ 架构关键字
49
+ - 按 app_id 作用域的角色/权限(App-scoped)
50
+ - Global-Mirror(global 是实体 app_id,不用 NULL)
51
+ - 复合外键强一致性(杜绝跨 App 脏绑定)
52
+ - RBAC 共享,业务表 App 私有
53
+
54
+ Global-Mirror 语义(重点)
55
+ - 'global' 必须存在且不可为 NULL。
56
+ - global 角色要跨 App 授权,必须在 global 权限字典中创建“同名镜像权限”。
57
+
58
+ 运维说明
59
+ - RBAC 表不开 RLS,避免 FORCE RLS 等运维坑与策略漂移。
60
+ - 具体 REVOKE/GRANT 收紧在后续迁移执行(请勿在此文件重复)。
61
+
62
+ ==================================================================================================== */
63
+
64
+
65
+ -- RBAC core tables (shared across all apps/modules)
66
+ -- Design:
67
+ -- - app-scoped roles & permissions (app_id)
68
+ -- - global is a first-class app_id (NOT NULL)
69
+ -- - role_permissions uses composite FK to prevent cross-app binding (physical integrity)
70
+
71
+ create table if not exists rbac.apps (
72
+ id text primary key, -- 'app1', 'app2', 'global'
73
+ name text not null
74
+ );
75
+
76
+ insert into rbac.apps (id, name)
77
+ values ('global', 'Global System')
78
+ on conflict (id) do nothing;
79
+
80
+ create table if not exists rbac.permissions (
81
+ id uuid primary key default gen_random_uuid(),
82
+ app_id text not null references rbac.apps(id) on delete cascade,
83
+ name text not null, -- 'order.read'
84
+ description text,
85
+ constraint permissions_unique_name unique (app_id, name),
86
+ constraint permissions_unique_id_app unique (id, app_id),
87
+ constraint permissions_id_not_sentinel check (id <> '00000000-0000-0000-0000-000000000000'::uuid)
88
+ );
89
+
90
+ create index if not exists idx_permissions_app_name
91
+ on rbac.permissions(app_id, name);
92
+
93
+ create table if not exists rbac.roles (
94
+ id uuid primary key default gen_random_uuid(),
95
+ app_id text not null references rbac.apps(id) on delete cascade,
96
+ name text not null, -- can be renamed safely; auth uses role_id
97
+ description text,
98
+ constraint roles_unique_name unique (app_id, name),
99
+ constraint roles_unique_id_app unique (id, app_id),
100
+ constraint roles_id_not_sentinel check (id <> '00000000-0000-0000-0000-000000000000'::uuid)
101
+ );
102
+
103
+ create table if not exists rbac.role_permissions (
104
+ app_id text not null references rbac.apps(id) on delete cascade,
105
+ role_id uuid not null,
106
+ permission_id uuid not null,
107
+ primary key (app_id, role_id, permission_id),
108
+
109
+ -- Physical consistency: role_id and permission_id must belong to the SAME app_id
110
+ constraint fk_role_consistency
111
+ foreign key (role_id, app_id) references rbac.roles(id, app_id) on delete cascade,
112
+ constraint fk_perm_consistency
113
+ foreign key (permission_id, app_id) references rbac.permissions(id, app_id) on delete cascade
114
+ );
115
+
116
+ -- Hot-path index for authorization check
117
+ create index if not exists idx_rp_lookup
118
+ on rbac.role_permissions(app_id, permission_id, role_id);
119
+
120
+ create table if not exists rbac.user_roles (
121
+ user_id uuid not null references auth.users(id) on delete cascade,
122
+ role_id uuid not null references rbac.roles(id) on delete cascade,
123
+ primary key (user_id, role_id)
124
+ );
125
+
126
+ create index if not exists idx_user_roles_uid on rbac.user_roles(user_id);
127
+ create index if not exists idx_user_roles_role on rbac.user_roles(role_id);
128
+
129
+ -- RBAC tables are protected via GRANT/REVOKE (not RLS) for operational safety.
130
+ alter table rbac.apps disable row level security;
131
+ alter table rbac.permissions disable row level security;
132
+ alter table rbac.roles disable row level security;
133
+ alter table rbac.role_permissions disable row level security;
134
+ alter table rbac.user_roles disable row level security;
@@ -0,0 +1,70 @@
1
+ /* =================================================================================================
2
+ ENGLISH COMMENT BLOCK (Architecture + Ops + Frontend Guidance)
3
+ ====================================================================================================
4
+
5
+ FILE PURPOSE
6
+ - Creates optional RBAC views for debugging/admin tooling:
7
+ - rbac.v_user_app_role_ids
8
+ - rbac.v_role_permissions
9
+
10
+ IMPORTANT NOTES
11
+ - Views do not automatically expose data to clients.
12
+ - Exposure is controlled by:
13
+ - Supabase Dashboard "Exposed Schemas"
14
+ - GRANT/REVOKE (applied in later migration)
15
+ - These views are primarily for:
16
+ - admin dashboards (service_role)
17
+ - internal troubleshooting / audits
18
+ - verifying role-permission bindings
19
+
20
+ FRONTEND GUIDANCE
21
+ - Do NOT rely on these views from client apps unless you intentionally expose them and design an admin API.
22
+ - Normal apps should rely on RLS + business-table queries.
23
+
24
+ ==================================================================================================== */
25
+
26
+
27
+ /* =================================================================================================
28
+ 中文注释区块(架构 + 运维 + 前端指引)
29
+ ====================================================================================================
30
+
31
+ 文件目的
32
+ - 创建 RBAC 辅助视图(用于调试/管理):
33
+ - rbac.v_user_app_role_ids(用户在各 App 下的角色)
34
+ - rbac.v_role_permissions(角色-权限展开)
35
+
36
+ 重要说明
37
+ - 视图不会自动暴露给客户端,是否可访问取决于:
38
+ - Supabase Exposed Schemas 配置
39
+ - 后续迁移中的 GRANT/REVOKE
40
+ - 这些视图建议只用于 service_role 的管理端或排障。
41
+
42
+ 前端提示
43
+ - 普通前端 App 不应直接访问这些视图。
44
+ - 正常业务只依赖 RLS + 业务表查询。
45
+
46
+ ==================================================================================================== */
47
+
48
+
49
+ -- Optional views for debugging/admin tooling.
50
+ -- Note: These views do not imply exposure; exposure is controlled by grants and exposed schemas.
51
+
52
+ create or replace view rbac.v_user_app_role_ids as
53
+ select
54
+ ur.user_id,
55
+ r.app_id,
56
+ r.id as role_id,
57
+ r.name as role_name
58
+ from rbac.user_roles ur
59
+ join rbac.roles r on r.id = ur.role_id;
60
+
61
+ create or replace view rbac.v_role_permissions as
62
+ select
63
+ rp.app_id,
64
+ rp.role_id,
65
+ ro.name as role_name,
66
+ rp.permission_id,
67
+ p.name as permission_name
68
+ from rbac.role_permissions rp
69
+ join rbac.roles ro on ro.id = rp.role_id
70
+ join rbac.permissions p on p.id = rp.permission_id;