@plutocms/supabase 0.7.0 → 0.7.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/CHANGELOG.md +7 -0
- package/app/plugins/pluto-extension.ts +3 -1
- package/package.json +1 -1
- package/server/api/migrations/run.post.ts +5 -2
- package/server/api/migrations/status.get.ts +5 -2
- package/server/api/permissions/me.get.ts +12 -1
- package/server/utils/admin-guard.ts +34 -11
- package/shared/types/supabase.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.1](https://github.com/plutocms/supabase/compare/v0.7.0...v0.7.1) (2026-09-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **permissions:** break the migrations-page bootstrap deadlock ([#55](https://github.com/plutocms/supabase/issues/55)) ([19f4338](https://github.com/plutocms/supabase/commit/19f43388827ba1f4699a2347726e56084c06f8ef))
|
|
9
|
+
|
|
3
10
|
## [0.7.0](https://github.com/plutocms/supabase/compare/v0.6.0...v0.7.0) (2026-09-12)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -36,10 +36,12 @@ export default defineNuxtPlugin(() => {
|
|
|
36
36
|
hasSettingsModified.value = Date.now()
|
|
37
37
|
},
|
|
38
38
|
},
|
|
39
|
+
// No 'system:migrate' capability: applying migrations stays admin-only,
|
|
40
|
+
// gated by requireAdmin (public.is_admin() directly), never a named
|
|
41
|
+
// capability — see server/utils/admin-guard.ts for why.
|
|
39
42
|
capabilities: [
|
|
40
43
|
{ id: 'settings-manage', key: 'settings:manage', label: 'Manage site settings' },
|
|
41
44
|
{ id: 'users-read', key: 'users:read', label: 'View all user accounts' },
|
|
42
|
-
{ id: 'system-migrate', key: 'system:migrate', label: 'Apply pending layer migrations' },
|
|
43
45
|
],
|
|
44
46
|
permissionsDriver: {
|
|
45
47
|
id: 'supabase',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PlutoMigrationFile } from '../../../shared/types/migrations'
|
|
2
2
|
import type { MigrationFileResult } from '../../utils/migrations'
|
|
3
|
-
import {
|
|
3
|
+
import { requireAdmin } from '../../utils/admin-guard'
|
|
4
4
|
import { persistDatabaseUrl } from '../../utils/env-file'
|
|
5
5
|
import { resolveConnectionString, runPendingMigrations } from '../../utils/migrations'
|
|
6
6
|
import { scrubConnectionString } from '../../utils/scrub-connection-string'
|
|
@@ -11,7 +11,10 @@ interface Payload {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export default defineEventHandler(async (event) => {
|
|
14
|
-
|
|
14
|
+
// requireAdmin specifically, not a named capability — see admin-guard.ts
|
|
15
|
+
// for why: this route must work even before 004_roles_and_capabilities.sql
|
|
16
|
+
// (which defines the capability system) has been applied.
|
|
17
|
+
await requireAdmin(event)
|
|
15
18
|
|
|
16
19
|
const body = await readBody<Payload | undefined>(event)
|
|
17
20
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { requireAdmin } from '../../utils/admin-guard'
|
|
2
2
|
import { getMigrationStatus } from '../../utils/pending-migrations'
|
|
3
3
|
|
|
4
4
|
export default defineEventHandler(async (event) => {
|
|
5
|
-
|
|
5
|
+
// requireAdmin specifically, not a named capability — see admin-guard.ts
|
|
6
|
+
// for why: this route must work even before 004_roles_and_capabilities.sql
|
|
7
|
+
// (which defines the capability system) has been applied.
|
|
8
|
+
await requireAdmin(event)
|
|
6
9
|
|
|
7
10
|
const status = await getMigrationStatus(event)
|
|
8
11
|
|
|
@@ -17,7 +17,18 @@ export default defineEventHandler(async (event) => {
|
|
|
17
17
|
const { data, error } = await client.rpc('my_capabilities')
|
|
18
18
|
|
|
19
19
|
if (error) {
|
|
20
|
-
|
|
20
|
+
// public.my_capabilities() is defined by 004_roles_and_capabilities.sql.
|
|
21
|
+
// A site that has upgraded this package but not yet applied that
|
|
22
|
+
// migration has no such function yet — that is an expected, temporary
|
|
23
|
+
// state during an upgrade, not a server error. Degrade to an empty
|
|
24
|
+
// capability list (the same shape a signed-out caller gets above)
|
|
25
|
+
// rather than 500ing: the caller already fails every can() check with
|
|
26
|
+
// an empty list, which is the correct, safe behavior until the pending
|
|
27
|
+
// migration is applied through /admin/migrations (itself gated by
|
|
28
|
+
// requireAdmin, not a capability, precisely so this state is always
|
|
29
|
+
// recoverable — see server/utils/admin-guard.ts).
|
|
30
|
+
console.error('my_capabilities() failed, returning an empty capability list:', error.message)
|
|
31
|
+
return { capabilities: [] as string[] }
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
return { capabilities: data ?? [] }
|
|
@@ -1,19 +1,42 @@
|
|
|
1
1
|
import type { H3Event } from 'h3'
|
|
2
|
-
import {
|
|
2
|
+
import { serverSupabaseClient, serverSupabaseUser } from '#supabase/server'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Guards a server route so only an admin
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Guards a server route so only an admin can call it.
|
|
6
|
+
*
|
|
7
|
+
* Calls `public.is_admin()` directly — deliberately NOT through
|
|
8
|
+
* `requireCapability`/`public.has_capability()`. `has_capability()` is
|
|
9
|
+
* defined by `004_roles_and_capabilities.sql`; a site that has upgraded
|
|
10
|
+
* this package but not yet applied that migration has no such function in
|
|
11
|
+
* its database, and every `requireCapability` call would throw. If
|
|
12
|
+
* `requireAdmin` routed through it too, that would 403 the one route
|
|
13
|
+
* (`/api/migrations/*`) an admin needs to actually apply the migration —
|
|
14
|
+
* a deadlock with no escape through the UI. `public.is_admin()` has existed
|
|
15
|
+
* since `002_admin_hardening.sql` and is only ever `create or replace`d,
|
|
16
|
+
* never dropped, so it is always safe to call regardless of which layer
|
|
17
|
+
* migrations have been applied. This is also why the migrations routes
|
|
18
|
+
* call `requireAdmin` specifically, and not a named capability like
|
|
19
|
+
* `system:migrate` — migrations are a bootstrapping concern and must never
|
|
20
|
+
* depend on the capability system migrations themselves create.
|
|
9
21
|
*
|
|
10
22
|
* Throws a 401 if there is no logged-in user, or a 403 if the user is not
|
|
11
|
-
* an admin. Returns the claims on success. See capability-guard.ts
|
|
12
|
-
* the user.sub vs user.id note
|
|
13
|
-
*
|
|
23
|
+
* an admin. Returns the user's claims on success. See capability-guard.ts
|
|
24
|
+
* for the same user.sub vs user.id note — this function has the same
|
|
25
|
+
* shape but never reads either field either.
|
|
14
26
|
*/
|
|
15
27
|
export async function requireAdmin(event: H3Event) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
const user = await serverSupabaseUser(event)
|
|
29
|
+
|
|
30
|
+
if (!user) {
|
|
31
|
+
throw createError({ statusCode: 401, statusMessage: 'You must be logged in.' })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const client = await serverSupabaseClient<Database>(event)
|
|
35
|
+
const { data, error } = await client.rpc('is_admin')
|
|
36
|
+
|
|
37
|
+
if (error || data !== true) {
|
|
38
|
+
throw createError({ statusCode: 403, statusMessage: 'Your account is not an admin.' })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return user
|
|
19
42
|
}
|
package/shared/types/supabase.ts
CHANGED