@plutocms/supabase 0.12.0 → 0.14.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/FEATURES.md +2 -1
  3. package/README.md +36 -28
  4. package/app/components/access/DeleteRoleModal.vue +1 -1
  5. package/app/components/access/EditRolesSlideover.vue +1 -1
  6. package/app/components/access/InviteModal.vue +2 -2
  7. package/app/components/access/RevokeModal.vue +1 -1
  8. package/app/components/access/RoleSlideover.vue +2 -2
  9. package/app/composables/migrations.ts +25 -5
  10. package/app/pages/admin/(auth)/signup.vue +10 -3
  11. package/app/pages/admin/settings/roles.vue +1 -1
  12. package/app/pages/admin/settings/users.vue +5 -5
  13. package/app/pages/admin/setup.vue +18 -16
  14. package/app/plugins/pluto-extension.ts +8 -8
  15. package/app/utils/api-failure.ts +16 -0
  16. package/app/utils/auth-driver.ts +2 -2
  17. package/i18n/locales/en.json +8 -3
  18. package/i18n/locales/pt-BR.json +8 -3
  19. package/package.json +3 -3
  20. package/server/api/{access → _pluto/supabase/access}/capabilities.get.ts +3 -3
  21. package/server/api/{access → _pluto/supabase/access}/roles/[key].delete.ts +2 -2
  22. package/server/api/{access → _pluto/supabase/access}/roles/[key].put.ts +4 -4
  23. package/server/api/{access → _pluto/supabase/access}/roles.get.ts +2 -2
  24. package/server/api/{access → _pluto/supabase/access}/roles.post.ts +4 -4
  25. package/server/api/{access → _pluto/supabase/access}/users/[id]/resend.post.ts +8 -8
  26. package/server/api/{access → _pluto/supabase/access}/users/[id]/roles.delete.ts +3 -3
  27. package/server/api/{access → _pluto/supabase/access}/users/[id]/roles.put.ts +4 -4
  28. package/server/api/{access → _pluto/supabase/access}/users/invite-link.post.ts +6 -6
  29. package/server/api/{access → _pluto/supabase/access}/users/invite.post.ts +8 -8
  30. package/server/api/{access → _pluto/supabase/access}/users.get.ts +2 -2
  31. package/server/api/{auth → _pluto/supabase/auth}/signup.post.ts +10 -5
  32. package/server/api/{profile → _pluto/supabase/me}/locale.get.ts +1 -1
  33. package/server/api/{profile/locale.post.ts → _pluto/supabase/me/locale.put.ts} +7 -2
  34. package/server/api/{migrations/status.get.ts → _pluto/supabase/migrations/index.get.ts} +2 -2
  35. package/server/api/{migrations → _pluto/supabase/migrations}/run.post.ts +21 -16
  36. package/server/api/{settings/update.post.ts → _pluto/supabase/settings/index.put.ts} +8 -4
  37. package/server/api/{settings/first_setup.get.ts → _pluto/supabase/setup/index.get.ts} +6 -5
  38. package/server/api/{setup/create.post.ts → _pluto/supabase/setup/index.post.ts} +13 -10
  39. package/server/utils/admin-guard.ts +1 -1
  40. package/server/utils/content-adapter.ts +111 -6
  41. package/server/utils/pending-migrations.ts +0 -0
  42. package/server/api/users/[id].get.ts +0 -26
  43. package/server/api/users/index.get.ts +0 -21
  44. /package/server/api/{permissions/me.get.ts → _pluto/supabase/me/capabilities.get.ts} +0 -0
  45. /package/server/api/{settings → _pluto/supabase/settings}/index.get.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.14.0](https://github.com/plutocms/supabase/compare/v0.13.0...v0.14.0) (2026-10-27)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * the routes `/api/access/**`, `/api/auth/signup`, `/api/migrations/**`, `/api/permissions/me`, `/api/profile/locale`, `/api/settings/**`, `/api/setup/create` and `/api/users/**` moved under `/api/_pluto/supabase/` (see the README), and failures are HTTP errors.
9
+
10
+ ### Features
11
+
12
+ * move the internal routes under /api/_pluto/supabase, and support the public API ([#104](https://github.com/plutocms/supabase/issues/104)) ([e7bf91c](https://github.com/plutocms/supabase/commit/e7bf91cd5a802247b4ec3b6938389b94a06ce3e3))
13
+
14
+ ## [0.13.0](https://github.com/plutocms/supabase/compare/v0.12.0...v0.13.0) (2026-10-25)
15
+
16
+
17
+ ### Features
18
+
19
+ * **deps:** raise @plutocms/pluto to ^0.20.0 ([#101](https://github.com/plutocms/supabase/issues/101)) ([ef39fb7](https://github.com/plutocms/supabase/commit/ef39fb717114f4302dd01a0877a8b2a68cf53613))
20
+
3
21
  ## [0.12.0](https://github.com/plutocms/supabase/compare/v0.11.3...v0.12.0) (2026-10-25)
4
22
 
5
23
 
package/FEATURES.md CHANGED
@@ -15,7 +15,8 @@ policies shared by every project here.
15
15
  link, roles editor), invites through Pluto mail, and a guide for the Supabase sign-in emails.
16
16
  @.claude/skills/users-and-roles/SKILL.md
17
17
  - Content adapter: a Supabase/PostgREST-backed `PlutoContentAdapter`, plugging any layer's
18
- declared content type into `@plutocms/pluto`'s generic content routes.
18
+ declared content type into `@plutocms/pluto`'s generic content routes. It supports query
19
+ filters, puts the timestamps on items, and reads the public API of core with an anonymous client.
19
20
  @.claude/skills/content-adapter/SKILL.md
20
21
  - Auth driver: plugs Supabase sign-in into the admin shell that pluto core owns (navbar, sign-in
21
22
  page, route guard), and keeps the setup guard in the driver.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # Supabase Layer for Pluto CMS
1
+ # Supabase Layer for Pluto
2
2
 
3
- This layer integrates Supabase as a backend service for Pluto CMS, providing a robust and scalable solution for managing content and data.
3
+ This layer integrates Supabase as a backend service for Pluto, providing a robust and scalable solution for managing content and data.
4
4
 
5
5
  ## Installation
6
6
 
@@ -14,28 +14,19 @@ SUPABASE_URL=your-supabase-url
14
14
  SUPABASE_KEY=your-publishable-supabase-key
15
15
  ```
16
16
 
17
- 2. Add the `@nuxtjs/supabase` and `supabase` dependencies to your project:
17
+ 2. Install the layer. It brings `@nuxtjs/supabase`, `@plutocms/pluto` and `@plutocms/utils`, so you do not add them:
18
18
 
19
19
  ```bash
20
- ## NPM
21
- npm i @nuxtjs/supabase supabase
22
-
23
- ## Yarn
24
- yarn add @nuxtjs/supabase supabase
25
-
26
- ## PNPM
27
- pnpm add @nuxtjs/supabase supabase
28
-
29
- ## Bun
30
- bun add @nuxtjs/supabase supabase
20
+ npm i @plutocms/supabase
21
+ # or pnpm add @plutocms/supabase, or bun add @plutocms/supabase
31
22
  ```
32
23
 
33
- 3. Include the Pluto Supabase layer via `extends` option in your `nuxt.config.ts`:
24
+ 3. Include the Pluto Supabase layer via `extends` option in your `nuxt.config.ts`. It is the only Pluto layer the app lists:
34
25
 
35
26
  ```ts
36
27
  // nuxt.config.ts
37
28
  export default defineNuxtConfig({
38
- extends: [['github:plutocms/supabase', { install: true }]],
29
+ extends: [['@plutocms/supabase', { install: true }]],
39
30
  })
40
31
  ```
41
32
 
@@ -59,7 +50,7 @@ entries. See "Migrating an app" in that skill.
59
50
  ### Invite users (access API)
60
51
 
61
52
  An admin invites a user by email and gives the user one or more roles. The routes are under
62
- `/api/access/**`. Every write needs an admin. This needs the migration
53
+ `/api/_pluto/supabase/access/**`. Every write needs an admin. This needs the migration
63
54
  `008_users_and_roles_admin.sql`, which the admin applies from `/admin/migrations`.
64
55
 
65
56
  Invites need no setup. Check one thing: in the Supabase dashboard (Authentication, then URL
@@ -116,19 +107,36 @@ come from Supabase. The Email tab has a guide to send them through Resend too.
116
107
 
117
108
  ### API Routes
118
109
 
119
- The layer includes API routes for interacting with Supabase services, such as authentication and media management.
120
-
121
- - GET `/api/settings`
122
- - POST `/api/settings/update`
123
- - POST `/api/auth/signup`
124
- - GET `/api/access/users`, GET `/api/access/roles` (permission `users:read`)
125
- - POST `/api/access/users/invite`, POST `/api/access/users/:id/resend` (admin; `locale` is the
110
+ The layer includes API routes for interacting with Supabase services, such as authentication and settings.
111
+ Every route lives under `/api/_pluto/supabase/`. Core blocks a cross-site write to `/api/_pluto/**`, so
112
+ a page of another site cannot call them. A failure is an HTTP error with a translation key, never a
113
+ `200` with `success: false`. These routes serve the admin. They are not a public API.
114
+
115
+ ### Public content API
116
+
117
+ The public content of the Supabase layers is read through the public API of core,
118
+ `GET /api/pluto/v1/content/<type>` (and `/<id or slug>`). See
119
+ [the public API](https://pluto.vulmix.dev/core/public-api). The routes are anonymous. This layer
120
+ reads for them with an anon client that has no user session, so Row Level Security applies as for
121
+ an anonymous visitor, and the answer is the same for everyone. A CDN can cache it. The adapter
122
+ never uses the service role key for a read. The adapter also supports the `filters` of a public
123
+ query, and puts the created, updated and published-at columns on each item, so a public list can
124
+ sort by date.
125
+
126
+ - GET and PUT `/api/_pluto/supabase/settings` (PUT needs `settings:manage`; it answers `{ message, data }`)
127
+ - POST `/api/_pluto/supabase/auth/signup`
128
+ - GET `/api/_pluto/supabase/migrations`, POST `/api/_pluto/supabase/migrations/run` (admin)
129
+ - GET `/api/_pluto/supabase/me/capabilities`
130
+ - GET and PUT `/api/_pluto/supabase/me/locale`
131
+ - GET `/api/_pluto/supabase/setup` (the setup state), POST `/api/_pluto/supabase/setup` (runs the first setup)
132
+ - GET `/api/_pluto/supabase/access/users`, GET `/api/_pluto/supabase/access/roles` (permission `users:read`)
133
+ - POST `/api/_pluto/supabase/access/users/invite`, POST `/api/_pluto/supabase/access/users/:id/resend` (admin; `locale` is the
126
134
  optional language of the email)
127
- - POST `/api/access/users/invite-link` (admin; needs the service role key; returns a one-time link
135
+ - POST `/api/_pluto/supabase/access/users/invite-link` (admin; needs the service role key; returns a one-time link
128
136
  and sends no email)
129
- - GET `/api/access/capabilities` (admin; says whether the service key and Pluto mail are set)
130
- - PUT and DELETE `/api/access/users/:id/roles` (admin; DELETE revokes the access)
131
- - POST `/api/access/roles`, PUT and DELETE `/api/access/roles/:key` (admin)
137
+ - GET `/api/_pluto/supabase/access/capabilities` (admin; says whether the service key and Pluto mail are set)
138
+ - PUT and DELETE `/api/_pluto/supabase/access/users/:id/roles` (admin; DELETE revokes the access)
139
+ - POST `/api/_pluto/supabase/access/roles`, PUT and DELETE `/api/_pluto/supabase/access/roles/:key` (admin)
132
140
 
133
141
  ### Types
134
142
 
@@ -23,7 +23,7 @@ async function remove() {
23
23
  working.value = true
24
24
 
25
25
  try {
26
- await $fetch(`/api/access/roles/${props.role.key}`, { method: 'DELETE' })
26
+ await $fetch(`/api/_pluto/supabase/access/roles/${props.role.key}`, { method: 'DELETE' })
27
27
 
28
28
  toast.add({
29
29
  title: t('supabase.access.toasts.roleDeleted'),
@@ -61,7 +61,7 @@ async function save() {
61
61
  saving.value = true
62
62
 
63
63
  try {
64
- await $fetch(`/api/access/users/${props.user.id}/roles`, {
64
+ await $fetch(`/api/_pluto/supabase/access/users/${props.user.id}/roles`, {
65
65
  method: 'PUT',
66
66
  body: { roleKeys: roleKeys.value },
67
67
  })
@@ -85,7 +85,7 @@ async function submit() {
85
85
  mailProblem.value = null
86
86
 
87
87
  try {
88
- const result = await $fetch<AccessInviteResult>('/api/access/users/invite', {
88
+ const result = await $fetch<AccessInviteResult>('/api/_pluto/supabase/access/users/invite', {
89
89
  method: 'POST',
90
90
  body: { ...body(), locale: emailLocale.value },
91
91
  })
@@ -140,7 +140,7 @@ async function copyLink() {
140
140
 
141
141
  try {
142
142
  const result = await $fetch<{ status: 'invited' | 'granted', link: string | null }>(
143
- '/api/access/users/invite-link',
143
+ '/api/_pluto/supabase/access/users/invite-link',
144
144
  { method: 'POST', body: body() }
145
145
  )
146
146
 
@@ -27,7 +27,7 @@ async function revoke() {
27
27
  working.value = true
28
28
 
29
29
  try {
30
- await $fetch(`/api/access/users/${props.user.id}/roles`, { method: 'DELETE' })
30
+ await $fetch(`/api/_pluto/supabase/access/users/${props.user.id}/roles`, { method: 'DELETE' })
31
31
 
32
32
  toast.add({
33
33
  title: t('supabase.access.toasts.revoked'),
@@ -131,12 +131,12 @@ async function save() {
131
131
 
132
132
  try {
133
133
  if (creating.value) {
134
- await $fetch('/api/access/roles', {
134
+ await $fetch('/api/_pluto/supabase/access/roles', {
135
135
  method: 'POST',
136
136
  body: { ...body, key: key.value },
137
137
  })
138
138
  } else {
139
- await $fetch(`/api/access/roles/${props.role!.key}`, { method: 'PUT', body })
139
+ await $fetch(`/api/_pluto/supabase/access/roles/${props.role!.key}`, { method: 'PUT', body })
140
140
  }
141
141
 
142
142
  toast.add({
@@ -53,7 +53,7 @@ export function useMigrations() {
53
53
  error,
54
54
  status: fetchStatus,
55
55
  refresh,
56
- } = useFetch<MigrationsStatusResponse>('/api/migrations/status', {
56
+ } = useFetch<MigrationsStatusResponse>('/api/_pluto/supabase/migrations', {
57
57
  key: 'pluto-migrations-status',
58
58
  })
59
59
 
@@ -70,10 +70,30 @@ export function useMigrations() {
70
70
  const pendingCount = computed(() => status.value?.pending.length ?? 0)
71
71
 
72
72
  async function runMigrations(connectionString?: string) {
73
- const result = await $fetch<RunMigrationsResponse>('/api/migrations/run', {
74
- method: 'POST',
75
- body: connectionString ? { connectionString } : undefined,
76
- })
73
+ let result: RunMigrationsResponse
74
+
75
+ try {
76
+ result = await $fetch<RunMigrationsResponse>('/api/_pluto/supabase/migrations/run', {
77
+ method: 'POST',
78
+ body: connectionString ? { connectionString } : undefined,
79
+ })
80
+ } catch (error) {
81
+ // The route answers a failed run with an HTTP error that carries
82
+ // `needsConnectionString` or `error` in its data. Turn it back into
83
+ // a failed result, so the page shows it as before. Any other error
84
+ // (for example a dropped connection) goes on to the caller.
85
+ const failure = failureData(error)
86
+
87
+ if (failure?.needsConnectionString !== true && typeof failure?.error !== 'string') {
88
+ throw error
89
+ }
90
+
91
+ result = {
92
+ success: false,
93
+ needsConnectionString: failure.needsConnectionString === true ? true : undefined,
94
+ error: typeof failure.error === 'string' ? failure.error : undefined,
95
+ }
96
+ }
77
97
 
78
98
  await refresh()
79
99
 
@@ -3,7 +3,7 @@ definePageMeta({
3
3
  layout: 'auth',
4
4
  })
5
5
 
6
- const { t } = usePlutoI18n()
6
+ const { t, te } = usePlutoI18n()
7
7
  const errorText = usePlutoErrorText()
8
8
 
9
9
  useHead({
@@ -36,7 +36,7 @@ async function submitForm() {
36
36
  isSubmitting.value = true
37
37
 
38
38
  try {
39
- await $fetch('/api/auth/signup', {
39
+ await $fetch('/api/_pluto/supabase/auth/signup', {
40
40
  method: 'POST',
41
41
  body: {
42
42
  email: form.value.email,
@@ -59,7 +59,14 @@ async function submitForm() {
59
59
 
60
60
  toast.add({
61
61
  title: t('supabase.auth.signup.failed'),
62
- description: errorText(error, 'supabase.auth.errors.unknown'),
62
+ description: resolveAuthErrorText(
63
+ {
64
+ code: failureData(error)?.code,
65
+ message: (error as { data?: { statusMessage?: string } }).data?.statusMessage
66
+ ?? errorText(error, 'supabase.auth.errors.unknown'),
67
+ },
68
+ { t, te }
69
+ ),
63
70
  icon: 'lucide:circle-x',
64
71
  color: 'error',
65
72
  })
@@ -29,7 +29,7 @@ async function load() {
29
29
  }
30
30
 
31
31
  try {
32
- const response = await $fetch<{ roles: AccessRole[] }>('/api/access/roles')
32
+ const response = await $fetch<{ roles: AccessRole[] }>('/api/_pluto/supabase/access/roles')
33
33
 
34
34
  roles.value = response.roles
35
35
  status.value = 'ready'
@@ -50,8 +50,8 @@ async function load() {
50
50
 
51
51
  try {
52
52
  const [usersResponse, rolesResponse] = await Promise.all([
53
- $fetch<{ users: AccessUser[] }>('/api/access/users'),
54
- $fetch<{ roles: AccessRole[] }>('/api/access/roles'),
53
+ $fetch<{ users: AccessUser[] }>('/api/_pluto/supabase/access/users'),
54
+ $fetch<{ roles: AccessRole[] }>('/api/_pluto/supabase/access/roles'),
55
55
  ])
56
56
 
57
57
  users.value = usersResponse.users
@@ -65,7 +65,7 @@ async function load() {
65
65
 
66
66
  async function loadCapabilities() {
67
67
  try {
68
- capabilities.value = await $fetch<AccessCapabilities>('/api/access/capabilities')
68
+ capabilities.value = await $fetch<AccessCapabilities>('/api/_pluto/supabase/access/capabilities')
69
69
  } catch {
70
70
  // The invite works without it. Only the "Copy invite link" action hides.
71
71
  capabilities.value = null
@@ -134,7 +134,7 @@ async function copyLink(user: AccessUser) {
134
134
  }
135
135
 
136
136
  try {
137
- const result = await $fetch<{ link: string | null }>('/api/access/users/invite-link', {
137
+ const result = await $fetch<{ link: string | null }>('/api/_pluto/supabase/access/users/invite-link', {
138
138
  method: 'POST',
139
139
  body: { email: user.email, roleKeys: user.roles },
140
140
  })
@@ -164,7 +164,7 @@ async function copyLink(user: AccessUser) {
164
164
  async function resend(user: AccessUser) {
165
165
  try {
166
166
  const result = await $fetch<{ emailSent?: boolean, link?: string }>(
167
- `/api/access/users/${user.id}/resend`,
167
+ `/api/_pluto/supabase/access/users/${user.id}/resend`,
168
168
  { method: 'POST', body: { locale: locale.value } }
169
169
  )
170
170
 
@@ -74,7 +74,7 @@ const currentStepSubmitLabel = computed(() => {
74
74
  })
75
75
 
76
76
  /**
77
- * Polls /api/settings/first_setup until the dev server responds again, and
77
+ * Polls /api/_pluto/supabase/setup until the dev server responds again, and
78
78
  * advances the stepper once it does.
79
79
  *
80
80
  * Used only to wait out the restart described in the `catch` block of
@@ -89,7 +89,7 @@ async function waitForServerAndAdvance(): Promise<boolean> {
89
89
  await new Promise((resolve) => setTimeout(resolve, delayMs))
90
90
 
91
91
  try {
92
- await $fetch('/api/settings/first_setup')
92
+ await $fetch('/api/_pluto/supabase/setup')
93
93
 
94
94
  toast.add({
95
95
  title: t('supabase.connection.reconnected'),
@@ -118,24 +118,13 @@ async function completeDatabaseSetup() {
118
118
  encodeURIComponent(databaseForm.value.password)
119
119
  )
120
120
 
121
- const data = await $fetch<any>('/api/setup/create', {
121
+ const data = await $fetch<any>('/api/_pluto/supabase/setup', {
122
122
  method: 'POST',
123
123
  body: {
124
124
  connectionString,
125
125
  },
126
126
  })
127
127
 
128
- if (data.success === false) {
129
- toast.add({
130
- title: t('supabase.setup.failed'),
131
- description: t('supabase.setup.failedDescription'),
132
- icon: 'lucide:circle-x',
133
- color: 'error',
134
- })
135
-
136
- return
137
- }
138
-
139
128
  if (data.persisted === false) {
140
129
  envNotPersisted.value = true
141
130
  submittedConnectionString.value = connectionString
@@ -150,7 +139,20 @@ async function completeDatabaseSetup() {
150
139
 
151
140
  stepper.value?.next()
152
141
  } catch (error) {
153
- // Writing DATABASE_URL to .env (the last step of /api/setup/create,
142
+ // The route answers a failed setup with an HTTP error that carries the
143
+ // scrubbed database error in its data.
144
+ if (typeof failureData(error)?.error === 'string') {
145
+ toast.add({
146
+ title: t('supabase.setup.failed'),
147
+ description: t('supabase.setup.failedDescription'),
148
+ icon: 'lucide:circle-x',
149
+ color: 'error',
150
+ })
151
+
152
+ return
153
+ }
154
+
155
+ // Writing DATABASE_URL to .env (the last step of /api/_pluto/supabase/setup,
154
156
  // after the schema and every layer migration already succeeded)
155
157
  // restarts the dev server. The response can be cut off by that
156
158
  // restart even though the setup itself worked. A dropped connection
@@ -326,7 +328,7 @@ function handleStepChange(step: number) {
326
328
  >
327
329
  <template #route>
328
330
  <ULink
329
- to="https://github.com/plutocms/supabase/blob/feat/setup-wizard/server/api/setup/create.post.ts"
331
+ to="https://github.com/plutocms/supabase/blob/main/server/api/_pluto/supabase/setup/index.post.ts"
330
332
  target="_blank"
331
333
  class="underline"
332
334
  >
@@ -36,7 +36,7 @@ export default defineNuxtPlugin(() => {
36
36
  async function loadSiteSettings() {
37
37
  try {
38
38
  const response = await $fetch<{ settings: Record<string, string> }>(
39
- '/api/settings'
39
+ '/api/_pluto/supabase/settings'
40
40
  )
41
41
  siteSettings.value = response.settings
42
42
  } catch {
@@ -53,7 +53,7 @@ export default defineNuxtPlugin(() => {
53
53
  let state: SetupState = 'unknown'
54
54
  try {
55
55
  state = setupStateFromResponse(
56
- await $fetch('/api/settings/first_setup')
56
+ await $fetch('/api/_pluto/supabase/setup')
57
57
  )
58
58
  } catch (error) {
59
59
  if (import.meta.dev) {
@@ -200,13 +200,13 @@ export default defineNuxtPlugin(() => {
200
200
  id: 'supabase',
201
201
  load: async () => {
202
202
  const response = await $fetch<{ settings: Record<string, string> }>(
203
- '/api/settings'
203
+ '/api/_pluto/supabase/settings'
204
204
  )
205
205
  return response.settings
206
206
  },
207
207
  save: async (patch) => {
208
- await $fetch('/api/settings/update', {
209
- method: 'POST',
208
+ await $fetch('/api/_pluto/supabase/settings', {
209
+ method: 'PUT',
210
210
  body: patch,
211
211
  })
212
212
 
@@ -229,12 +229,12 @@ export default defineNuxtPlugin(() => {
229
229
  localeDriver: {
230
230
  id: 'supabase',
231
231
  load: async () => {
232
- const response = await $fetch<{ locale: string | null }>('/api/profile/locale')
232
+ const response = await $fetch<{ locale: string | null }>('/api/_pluto/supabase/me/locale')
233
233
  return response.locale
234
234
  },
235
235
  save: async (locale) => {
236
236
  try {
237
- await $fetch('/api/profile/locale', { method: 'POST', body: { locale } })
237
+ await $fetch('/api/_pluto/supabase/me/locale', { method: 'PUT', body: { locale } })
238
238
  } catch (error) {
239
239
  throw new Error(errorText(error))
240
240
  }
@@ -243,7 +243,7 @@ export default defineNuxtPlugin(() => {
243
243
  permissionsDriver: {
244
244
  id: 'supabase',
245
245
  load: async () => {
246
- const response = await $fetch<{ capabilities: string[] }>('/api/permissions/me')
246
+ const response = await $fetch<{ capabilities: string[] }>('/api/_pluto/supabase/me/capabilities')
247
247
  return response.capabilities
248
248
  },
249
249
  },
@@ -0,0 +1,16 @@
1
+ // Reads the extra data of a failed call to this layer's routes. This file does
2
+ // not use Nuxt auto-imports, so plain Vitest can import it.
3
+
4
+ /**
5
+ * Returns the `data` that a route put in its `createPlutoError`. A `$fetch`
6
+ * error holds the whole response body in `data`, so the extra data sits in
7
+ * `data.data`. Returns `undefined` when the error has none.
8
+ */
9
+ export function failureData(error: unknown): Record<string, unknown> | undefined {
10
+ const body = (error as { data?: unknown } | null | undefined)?.data
11
+ const data = (body as { data?: unknown } | null | undefined)?.data
12
+
13
+ return typeof data === 'object' && data !== null
14
+ ? (data as Record<string, unknown>)
15
+ : undefined
16
+ }
@@ -67,11 +67,11 @@ export const SUPABASE_PUBLIC_PATHS: PlutoAuthPublicPath[] = [
67
67
  /** The page that opens after a sign-in with no (safe) redirect. */
68
68
  export const SUPABASE_SIGN_IN_FALLBACK = '/admin/home'
69
69
 
70
- /** The setup state that the setup guard reads from `/api/settings/first_setup`. */
70
+ /** The setup state that the setup guard reads from `/api/_pluto/supabase/setup`. */
71
71
  export type SetupState = 'pending' | 'done' | 'unknown'
72
72
 
73
73
  /**
74
- * Reads the answer of `/api/settings/first_setup`. A failed call, or a
74
+ * Reads the answer of `/api/_pluto/supabase/setup`. A failed call, or a
75
75
  * response with no `is_first_setup` flag, is `unknown`.
76
76
  */
77
77
  export function setupStateFromResponse(data: unknown): SetupState {
@@ -9,12 +9,17 @@
9
9
  "missingPermission": "Your account is missing the \"{capability}\" permission.",
10
10
  "setupCompleted": "Setup has already been completed.",
11
11
  "coreSchemaMissing": "Core schema not found. Reinstall @plutocms/supabase and restart the dev server.",
12
- "fetchUsers": "Error fetching users: {message}",
13
- "fetchUser": "Error fetching user: {message}",
14
12
  "localeUnsupported": "This language is not supported.",
15
13
  "duplicateRecord": "A record with this value already exists.",
16
14
  "missingReference": "This references a record that does not exist.",
17
- "migrationStatus": "Error reading migration status: {message}"
15
+ "migrationStatus": "Error reading migration status: {message}",
16
+ "signupFailed": "Could not create the account.",
17
+ "connectionStringRequired": "A database connection string is required.",
18
+ "migrationsFailed": "Migrations failed: {message}",
19
+ "setupFailed": "Database setup failed: {message}",
20
+ "setupStateRead": "Could not read the setup state: {message}",
21
+ "settingsSave": "Could not save the settings: {message}",
22
+ "localeSave": "Could not save your language: {message}"
18
23
  },
19
24
  "auth": {
20
25
  "backToLogin": "Back to Login",
@@ -9,12 +9,17 @@
9
9
  "missingPermission": "Sua conta não tem a permissão \"{capability}\".",
10
10
  "setupCompleted": "A configuração já foi concluída.",
11
11
  "coreSchemaMissing": "Esquema principal não encontrado. Reinstale @plutocms/supabase e reinicie o servidor de desenvolvimento.",
12
- "fetchUsers": "Erro ao buscar os usuários: {message}",
13
- "fetchUser": "Erro ao buscar o usuário: {message}",
14
12
  "localeUnsupported": "Este idioma não está disponível.",
15
13
  "duplicateRecord": "Já existe um registro com este valor.",
16
14
  "missingReference": "Isto aponta para um registro que não existe.",
17
- "migrationStatus": "Erro ao ler o status das migrações: {message}"
15
+ "migrationStatus": "Erro ao ler o status das migrações: {message}",
16
+ "signupFailed": "Não foi possível criar a conta.",
17
+ "connectionStringRequired": "Informe a string de conexão do banco de dados.",
18
+ "migrationsFailed": "As migrações falharam: {message}",
19
+ "setupFailed": "A configuração do banco de dados falhou: {message}",
20
+ "setupStateRead": "Não foi possível ler o estado da configuração: {message}",
21
+ "settingsSave": "Não foi possível salvar as configurações: {message}",
22
+ "localeSave": "Não foi possível salvar o seu idioma: {message}"
18
23
  },
19
24
  "auth": {
20
25
  "backToLogin": "Voltar para o login",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@plutocms/supabase",
3
3
  "type": "module",
4
- "version": "0.12.0",
4
+ "version": "0.14.0",
5
5
  "trustedDependencies": [
6
6
  "@parcel/watcher",
7
7
  "@plutocms/pluto",
@@ -11,7 +11,7 @@
11
11
  "unrs-resolver",
12
12
  "vue-demi"
13
13
  ],
14
- "description": "Supabase backend layer for Pluto CMS",
14
+ "description": "Supabase backend layer for Pluto",
15
15
  "author": "Victor Ribeiro",
16
16
  "homepage": "https://github.com/plutocms/supabase#readme",
17
17
  "repository": {
@@ -39,7 +39,7 @@
39
39
  "@nuxt/eslint": "^1.17.0",
40
40
  "@nuxt/ui": "^4.12.0",
41
41
  "@nuxtjs/supabase": "^2.1.0",
42
- "@plutocms/pluto": "^0.18.0",
42
+ "@plutocms/pluto": "^0.27.0",
43
43
  "@plutocms/utils": "^0.2.1",
44
44
  "@supabase/supabase-js": "^2.117.3",
45
45
  "postgres": "^3.4.9",
@@ -1,6 +1,6 @@
1
- import { plutoMailReady } from '../../utils/access-mail'
2
- import { requireAdminRole } from '../../utils/admin-role-guard'
3
- import { optionalServiceClient } from '../../utils/service-client'
1
+ import { plutoMailReady } from '../../../../utils/access-mail'
2
+ import { requireAdminRole } from '../../../../utils/admin-role-guard'
3
+ import { optionalServiceClient } from '../../../../utils/service-client'
4
4
 
5
5
  /**
6
6
  * What the invite UI can offer, for an admin: whether the service role key is
@@ -1,6 +1,6 @@
1
1
  import { serverSupabaseClient } from '#supabase/server'
2
- import { mapAccessError } from '../../../utils/access-errors'
3
- import { requireAdminRole } from '../../../utils/admin-role-guard'
2
+ import { mapAccessError } from '../../../../../utils/access-errors'
3
+ import { requireAdminRole } from '../../../../../utils/admin-role-guard'
4
4
 
5
5
  /** Deletes a role. It answers with the number of users who lost the role. */
6
6
  export default defineEventHandler(async (event) => {
@@ -1,8 +1,8 @@
1
1
  import { serverSupabaseClient } from '#supabase/server'
2
- import { mapAccessError } from '../../../utils/access-errors'
3
- import { roleBodySchema } from '../../../utils/access-invite'
4
- import { requireAdminRole } from '../../../utils/admin-role-guard'
5
- import { parseBody } from '../../../utils/validate'
2
+ import { mapAccessError } from '../../../../../utils/access-errors'
3
+ import { roleBodySchema } from '../../../../../utils/access-invite'
4
+ import { requireAdminRole } from '../../../../../utils/admin-role-guard'
5
+ import { parseBody } from '../../../../../utils/validate'
6
6
 
7
7
  /** Edits a role and replaces its capabilities. The `admin` role is refused. */
8
8
  export default defineEventHandler(async (event) => {
@@ -1,6 +1,6 @@
1
1
  import { serverSupabaseClient } from '#supabase/server'
2
- import { mapAccessError } from '../../utils/access-errors'
3
- import { requireCapability } from '../../utils/capability-guard'
2
+ import { mapAccessError } from '../../../../utils/access-errors'
3
+ import { requireCapability } from '../../../../utils/capability-guard'
4
4
 
5
5
  /** The roles, with their capabilities and the number of users. */
6
6
  export default defineEventHandler(async (event) => {
@@ -1,8 +1,8 @@
1
1
  import { serverSupabaseClient } from '#supabase/server'
2
- import { mapAccessError } from '../../utils/access-errors'
3
- import { createRoleBodySchema } from '../../utils/access-invite'
4
- import { requireAdminRole } from '../../utils/admin-role-guard'
5
- import { parseBody } from '../../utils/validate'
2
+ import { mapAccessError } from '../../../../utils/access-errors'
3
+ import { createRoleBodySchema } from '../../../../utils/access-invite'
4
+ import { requireAdminRole } from '../../../../utils/admin-role-guard'
5
+ import { parseBody } from '../../../../utils/validate'
6
6
 
7
7
  /** Creates a role. A new role cannot hold the wildcard. */
8
8
  export default defineEventHandler(async (event) => {