@sunboyoo/supabase-rbac 1.0.3 → 1.1.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/README.md
CHANGED
|
@@ -86,12 +86,10 @@ npx rbac-init --dest ./supabase/migrations
|
|
|
86
86
|
npx rbac-init --dry-run
|
|
87
87
|
npx rbac-init --force
|
|
88
88
|
npx rbac-init --verbose
|
|
89
|
-
npx rbac-init --with-examples
|
|
90
89
|
```
|
|
91
90
|
|
|
92
91
|
> ✅ **推荐**:默认不覆盖已有文件(避免误伤本地改动)。
|
|
93
92
|
> 如需覆盖,请显式 `--force`。
|
|
94
|
-
> 说明:默认不复制示例迁移(包含 app1 orders 例子)。如需示例,请加 `--with-examples`。
|
|
95
93
|
|
|
96
94
|
---
|
|
97
95
|
|
|
@@ -155,6 +153,62 @@ await supabase.auth.refreshSession();
|
|
|
155
153
|
|
|
156
154
|
否则用户 token 仍是旧 claims。
|
|
157
155
|
|
|
156
|
+
### 4) 业务表 RLS 配置示例
|
|
157
|
+
|
|
158
|
+
在业务表上配置 RLS 策略调用 `rbac.has_perm()`:
|
|
159
|
+
|
|
160
|
+
```sql
|
|
161
|
+
-- Example business table (app1-private) and RLS policies.
|
|
162
|
+
-- In real projects, you usually create one migration per module/table group.
|
|
163
|
+
|
|
164
|
+
create table if not exists public.orders (
|
|
165
|
+
id uuid primary key default gen_random_uuid(),
|
|
166
|
+
created_at timestamptz not null default now(),
|
|
167
|
+
customer text,
|
|
168
|
+
amount_cents int not null default 0
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
alter table public.orders enable row level security;
|
|
172
|
+
|
|
173
|
+
drop policy if exists app1_order_select on public.orders;
|
|
174
|
+
create policy app1_order_select
|
|
175
|
+
on public.orders
|
|
176
|
+
for select
|
|
177
|
+
using (
|
|
178
|
+
(select rbac.has_perm('app1', 'order.read'))
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
drop policy if exists app1_order_insert on public.orders;
|
|
182
|
+
create policy app1_order_insert
|
|
183
|
+
on public.orders
|
|
184
|
+
for insert
|
|
185
|
+
with check (
|
|
186
|
+
(select rbac.has_perm('app1', 'order.create'))
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
drop policy if exists app1_order_update on public.orders;
|
|
190
|
+
create policy app1_order_update
|
|
191
|
+
on public.orders
|
|
192
|
+
for update
|
|
193
|
+
using (
|
|
194
|
+
(select rbac.has_perm('app1', 'order.update'))
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
drop policy if exists app1_order_delete on public.orders;
|
|
198
|
+
create policy app1_order_delete
|
|
199
|
+
on public.orders
|
|
200
|
+
for delete
|
|
201
|
+
using (
|
|
202
|
+
(select rbac.has_perm('app1', 'order.delete'))
|
|
203
|
+
);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**要点:**
|
|
207
|
+
|
|
208
|
+
* 用 `(select rbac.has_perm(...))` 包裹:诱导 PostgreSQL 优化器做 InitPlan(一次计算)
|
|
209
|
+
* 即使未完全 InitPlan,`has_perm` 内部也有事务级缓存,避免 N+1 查询
|
|
210
|
+
* 客户端正常 CRUD;无权限时:SELECT 返回 0 行,INSERT/UPDATE/DELETE 返回 403
|
|
211
|
+
|
|
158
212
|
---
|
|
159
213
|
|
|
160
214
|
## 前端集成(React)
|
package/bin/rbac-init.js
CHANGED
|
@@ -1,80 +1,99 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/* ================================================================================================
|
|
3
|
-
EN: rbac-init CLI
|
|
4
|
-
-
|
|
5
|
-
中文:rbac-init 命令
|
|
6
|
-
- 将包内 migrations 安全复制到项目的 supabase/migrations
|
|
3
|
+
EN: rbac-init CLI - Copy bundled SQL migrations into target supabase/migrations folder.
|
|
4
|
+
中文:rbac-init 命令 - 将包内 migrations 复制到项目的 supabase/migrations
|
|
7
5
|
================================================================================================ */
|
|
8
6
|
|
|
9
7
|
import fs from "node:fs";
|
|
10
8
|
import fsp from "node:fs/promises";
|
|
11
9
|
import path from "node:path";
|
|
12
|
-
import process from "node:process";
|
|
13
10
|
import { fileURLToPath } from "node:url";
|
|
14
11
|
|
|
12
|
+
const PREFIX = "[rbac-init]";
|
|
13
|
+
|
|
15
14
|
function parseArgs(argv) {
|
|
16
|
-
const out = { dest: null, force: false, dryRun: false, verbose: false
|
|
15
|
+
const out = { dest: null, force: false, dryRun: false, verbose: false };
|
|
17
16
|
for (let i = 2; i < argv.length; i++) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
const arg = argv[i];
|
|
18
|
+
switch (arg) {
|
|
19
|
+
case "--dest":
|
|
20
|
+
out.dest = argv[++i] ?? null;
|
|
21
|
+
break;
|
|
22
|
+
case "--force":
|
|
23
|
+
case "-f":
|
|
24
|
+
out.force = true;
|
|
25
|
+
break;
|
|
26
|
+
case "--dry-run":
|
|
27
|
+
out.dryRun = true;
|
|
28
|
+
break;
|
|
29
|
+
case "--verbose":
|
|
30
|
+
case "-v":
|
|
31
|
+
out.verbose = true;
|
|
32
|
+
break;
|
|
33
|
+
case "--help":
|
|
34
|
+
case "-h":
|
|
35
|
+
console.log(`rbac-init
|
|
26
36
|
|
|
27
37
|
Usage:
|
|
28
|
-
rbac-init [--dest <path>] [--force] [--dry-run] [--verbose]
|
|
38
|
+
rbac-init [--dest <path>] [--force] [--dry-run] [--verbose]
|
|
29
39
|
|
|
30
40
|
Defaults:
|
|
31
41
|
--dest ./supabase/migrations
|
|
32
42
|
|
|
33
43
|
Options:
|
|
34
|
-
--force
|
|
35
|
-
--dry-run
|
|
36
|
-
--verbose
|
|
37
|
-
--with-examples include example migrations (off by default)
|
|
44
|
+
-f, --force Overwrite existing files if content differs
|
|
45
|
+
--dry-run Print actions without writing
|
|
46
|
+
-v, --verbose Print detailed logs
|
|
38
47
|
`);
|
|
39
|
-
|
|
48
|
+
process.exit(0);
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
return out;
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
async
|
|
46
|
-
try {
|
|
47
|
-
|
|
54
|
+
const exists = async (p) => {
|
|
55
|
+
try {
|
|
56
|
+
await fsp.access(p, fs.constants.F_OK);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
48
62
|
|
|
49
|
-
async
|
|
50
|
-
try {
|
|
51
|
-
|
|
63
|
+
const readText = async (p) => {
|
|
64
|
+
try {
|
|
65
|
+
return await fsp.readFile(p, "utf8");
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
52
70
|
|
|
53
71
|
async function main() {
|
|
54
72
|
const args = parseArgs(process.argv);
|
|
55
|
-
const
|
|
56
|
-
const destDir = path.resolve(cwd, args.dest ?? path.join("supabase", "migrations"));
|
|
73
|
+
const destDir = path.resolve(process.cwd(), args.dest ?? path.join("supabase", "migrations"));
|
|
57
74
|
|
|
58
|
-
const
|
|
59
|
-
const __dirname = path.dirname(__filename);
|
|
75
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
60
76
|
const srcDir = path.resolve(__dirname, "..", "migrations");
|
|
61
77
|
|
|
62
78
|
if (!(await exists(srcDir))) {
|
|
63
|
-
console.error(
|
|
79
|
+
console.error(`${PREFIX} migrations folder not found: ${srcDir}`);
|
|
64
80
|
process.exit(1);
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
if (!(await exists(destDir))) {
|
|
68
|
-
if (args.dryRun)
|
|
69
|
-
|
|
84
|
+
if (args.dryRun) {
|
|
85
|
+
console.log(`[dry-run] mkdir -p ${destDir}`);
|
|
86
|
+
} else {
|
|
87
|
+
await fsp.mkdir(destDir, { recursive: true });
|
|
88
|
+
}
|
|
70
89
|
}
|
|
71
90
|
|
|
72
91
|
const files = (await fsp.readdir(srcDir))
|
|
73
92
|
.filter((f) => f.endsWith(".sql"))
|
|
74
|
-
.filter((f) => args.withExamples || !/example/i.test(f))
|
|
75
93
|
.sort();
|
|
94
|
+
|
|
76
95
|
if (files.length === 0) {
|
|
77
|
-
console.log(
|
|
96
|
+
console.log(`${PREFIX} no .sql files found.`);
|
|
78
97
|
return;
|
|
79
98
|
}
|
|
80
99
|
|
|
@@ -88,17 +107,17 @@ async function main() {
|
|
|
88
107
|
const srcText = await readText(src);
|
|
89
108
|
const dstText = dstExists ? await readText(dst) : null;
|
|
90
109
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (same) {
|
|
110
|
+
// Skip if content is identical
|
|
111
|
+
if (dstExists && srcText === dstText) {
|
|
94
112
|
skipped++;
|
|
95
|
-
if (args.verbose) console.log(
|
|
113
|
+
if (args.verbose) console.log(`${PREFIX} skip (same): ${file}`);
|
|
96
114
|
continue;
|
|
97
115
|
}
|
|
98
116
|
|
|
117
|
+
// Skip if exists and not forced
|
|
99
118
|
if (dstExists && !args.force) {
|
|
100
119
|
skipped++;
|
|
101
|
-
console.log(
|
|
120
|
+
console.log(`${PREFIX} skip (use --force): ${file}`);
|
|
102
121
|
continue;
|
|
103
122
|
}
|
|
104
123
|
|
|
@@ -108,15 +127,19 @@ async function main() {
|
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
await fsp.copyFile(src, dst);
|
|
111
|
-
if (dstExists)
|
|
112
|
-
|
|
130
|
+
if (dstExists) {
|
|
131
|
+
overwritten++;
|
|
132
|
+
} else {
|
|
133
|
+
copied++;
|
|
134
|
+
}
|
|
135
|
+
console.log(`${PREFIX} ${dstExists ? "overwrote" : "copied"}: ${file}`);
|
|
113
136
|
}
|
|
114
137
|
|
|
115
|
-
console.log(
|
|
116
|
-
console.log(
|
|
138
|
+
console.log(`${PREFIX} done. copied=${copied}, overwritten=${overwritten}, skipped=${skipped}`);
|
|
139
|
+
console.log(`${PREFIX} next: run "supabase db push"`);
|
|
117
140
|
}
|
|
118
141
|
|
|
119
142
|
main().catch((e) => {
|
|
120
|
-
console.error(
|
|
143
|
+
console.error(`${PREFIX} error:`, e);
|
|
121
144
|
process.exit(1);
|
|
122
145
|
});
|
|
@@ -126,11 +126,11 @@ security definer
|
|
|
126
126
|
set search_path = ''
|
|
127
127
|
as $$
|
|
128
128
|
declare
|
|
129
|
-
_res_key text := 'rbac.res.' || md5(coalesce(target_app_id,'') || '|' || coalesce(required_perm_name,''));
|
|
130
|
-
_t_roles_key text := 'rbac.rids.' || md5('t|' || coalesce(target_app_id,''));
|
|
131
|
-
_g_roles_key text := 'rbac.rids.' || md5('g|global');
|
|
132
|
-
_t_pid_key text := 'rbac.pid.' || md5('t|' || coalesce(target_app_id,'') || '|' || coalesce(required_perm_name,''));
|
|
133
|
-
_g_pid_key text := 'rbac.pid.' || md5('g|global|' || coalesce(required_perm_name,''));
|
|
129
|
+
_res_key text := 'rbac.res.h' || md5(coalesce(target_app_id,'') || '|' || coalesce(required_perm_name,''));
|
|
130
|
+
_t_roles_key text := 'rbac.rids.h' || md5('t|' || coalesce(target_app_id,''));
|
|
131
|
+
_g_roles_key text := 'rbac.rids.h' || md5('g|global');
|
|
132
|
+
_t_pid_key text := 'rbac.pid.h' || md5('t|' || coalesce(target_app_id,'') || '|' || coalesce(required_perm_name,''));
|
|
133
|
+
_g_pid_key text := 'rbac.pid.h' || md5('g|global|' || coalesce(required_perm_name,''));
|
|
134
134
|
|
|
135
135
|
_target_role_ids uuid[];
|
|
136
136
|
_global_role_ids uuid[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sunboyoo/supabase-rbac",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Industrial-grade RBAC helper for Supabase multi-app projects (JWT role_ids + optional permission RPC + React hooks + pg manager + CLI).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -106,4 +106,4 @@
|
|
|
106
106
|
"access": "public",
|
|
107
107
|
"registry": "https://registry.npmjs.org/"
|
|
108
108
|
}
|
|
109
|
-
}
|
|
109
|
+
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/* =================================================================================================
|
|
2
|
-
ENGLISH COMMENT BLOCK (Architecture + Ops + Frontend Guidance)
|
|
3
|
-
====================================================================================================
|
|
4
|
-
|
|
5
|
-
FILE PURPOSE
|
|
6
|
-
- Example business table (app1-private) + RLS policies demonstrating how to use rbac.has_perm().
|
|
7
|
-
- This file represents how each app/module should apply RLS on its own private tables.
|
|
8
|
-
|
|
9
|
-
HOW TO USE THIS PATTERN (Recommended)
|
|
10
|
-
- For each app module:
|
|
11
|
-
- Create its private tables (no need to add app_id column if the table is truly app-private).
|
|
12
|
-
- Enable RLS.
|
|
13
|
-
- Write policies that call:
|
|
14
|
-
(select rbac.has_perm('<app_id>', '<permission_name>'))
|
|
15
|
-
|
|
16
|
-
WHY WRAP WITH (SELECT ...)
|
|
17
|
-
- Encourages PostgreSQL optimizer to evaluate the function once per statement (InitPlan best-effort).
|
|
18
|
-
- Even if the optimizer does not fully InitPlan it, has_perm has transaction-local caching to prevent
|
|
19
|
-
N+1 RBAC lookups.
|
|
20
|
-
|
|
21
|
-
FRONTEND BEHAVIOR
|
|
22
|
-
- Client does normal CRUD calls. If permission is missing:
|
|
23
|
-
- SELECT returns 0 rows or 403 depending on query/path.
|
|
24
|
-
- INSERT/UPDATE/DELETE returns 403.
|
|
25
|
-
- After admin changes roles/permissions:
|
|
26
|
-
- user must refresh JWT: supabase.auth.refreshSession()
|
|
27
|
-
|
|
28
|
-
==================================================================================================== */
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/* =================================================================================================
|
|
32
|
-
中文注释区块(架构 + 运维 + 前端指引)
|
|
33
|
-
====================================================================================================
|
|
34
|
-
|
|
35
|
-
文件目的
|
|
36
|
-
- 示例业务表(app1 私有)及 RLS 策略,演示如何调用 rbac.has_perm()。
|
|
37
|
-
- 真实项目中建议每个 App/模块的业务表和策略独立迁移文件维护。
|
|
38
|
-
|
|
39
|
-
推荐用法
|
|
40
|
-
- 每个 App:
|
|
41
|
-
- 建立自己的私有业务表(如果确实是 app-private 表,不需要 app_id 字段)。
|
|
42
|
-
- 开启 RLS。
|
|
43
|
-
- 策略中调用:
|
|
44
|
-
(select rbac.has_perm('<app_id>', '<permission_name>'))
|
|
45
|
-
|
|
46
|
-
为什么用 (SELECT ...) 包裹
|
|
47
|
-
- 诱导优化器把函数当作 InitPlan(尽力做到一次计算)。
|
|
48
|
-
- 即使没有完全 InitPlan,has_perm 内部也有事务级缓存,能避免 N+1 权限查询。
|
|
49
|
-
|
|
50
|
-
前端表现
|
|
51
|
-
- 前端正常 CRUD。无权限时:
|
|
52
|
-
- SELECT 可能返回 0 行或 403(取决于接口路径/查询方式)
|
|
53
|
-
- INSERT/UPDATE/DELETE 返回 403
|
|
54
|
-
- 管理端改完角色/权限后:
|
|
55
|
-
- 用户需刷新 JWT:supabase.auth.refreshSession()
|
|
56
|
-
|
|
57
|
-
==================================================================================================== */
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
-- Example business table (app1-private) and RLS policies.
|
|
61
|
-
-- In real projects, you usually create one migration per module/table group.
|
|
62
|
-
|
|
63
|
-
create table if not exists public.orders (
|
|
64
|
-
id uuid primary key default gen_random_uuid(),
|
|
65
|
-
created_at timestamptz not null default now(),
|
|
66
|
-
customer text,
|
|
67
|
-
amount_cents int not null default 0
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
alter table public.orders enable row level security;
|
|
71
|
-
|
|
72
|
-
drop policy if exists app1_order_select on public.orders;
|
|
73
|
-
create policy app1_order_select
|
|
74
|
-
on public.orders
|
|
75
|
-
for select
|
|
76
|
-
using (
|
|
77
|
-
(select rbac.has_perm('app1', 'order.read'))
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
drop policy if exists app1_order_insert on public.orders;
|
|
81
|
-
create policy app1_order_insert
|
|
82
|
-
on public.orders
|
|
83
|
-
for insert
|
|
84
|
-
with check (
|
|
85
|
-
(select rbac.has_perm('app1', 'order.create'))
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
drop policy if exists app1_order_update on public.orders;
|
|
89
|
-
create policy app1_order_update
|
|
90
|
-
on public.orders
|
|
91
|
-
for update
|
|
92
|
-
using (
|
|
93
|
-
(select rbac.has_perm('app1', 'order.update'))
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
drop policy if exists app1_order_delete on public.orders;
|
|
97
|
-
create policy app1_order_delete
|
|
98
|
-
on public.orders
|
|
99
|
-
for delete
|
|
100
|
-
using (
|
|
101
|
-
(select rbac.has_perm('app1', 'order.delete'))
|
|
102
|
-
);
|