@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 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,7 +1,7 @@
1
1
  {
2
2
  "name": "@plutocms/supabase",
3
3
  "type": "module",
4
- "version": "0.7.0",
4
+ "version": "0.7.1",
5
5
  "trustedDependencies": [
6
6
  "@parcel/watcher",
7
7
  "@plutocms/pluto",
@@ -1,6 +1,6 @@
1
1
  import type { PlutoMigrationFile } from '../../../shared/types/migrations'
2
2
  import type { MigrationFileResult } from '../../utils/migrations'
3
- import { requireCapability } from '../../utils/capability-guard'
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
- await requireCapability(event, 'system:migrate')
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 { requireCapability } from '../../utils/capability-guard'
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
- await requireCapability(event, 'system:migrate')
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
- throw createError({ statusCode: 500, statusMessage: error.message })
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 { ALL_CAPABILITIES, requireCapability } from './capability-guard'
2
+ import { serverSupabaseClient, serverSupabaseUser } from '#supabase/server'
3
3
 
4
4
  /**
5
- * Guards a server route so only an admin (or a holder of every
6
- * capability, which is what the built-in admin role grants) can call it.
7
- * A thin alias over requireCapability, kept for the routes that ask for
8
- * "admin, full stop" rather than one named capability.
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 for
12
- * the user.sub vs user.id note: this function forwards straight into
13
- * requireCapability, which never reads either field.
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
- return requireCapability(event, ALL_CAPABILITIES, {
17
- message: 'Your account is not an admin.',
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
  }
@@ -360,6 +360,10 @@ export type Database = {
360
360
  Args: Record<PropertyKey, never>
361
361
  Returns: string[]
362
362
  }
363
+ is_admin: {
364
+ Args: Record<PropertyKey, never>
365
+ Returns: boolean
366
+ }
363
367
  }
364
368
  Enums: {
365
369
  thealthcheck: "first_setup"