@plutocms/supabase 0.10.0 → 0.11.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,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.1](https://github.com/plutocms/supabase/compare/v0.11.0...v0.11.1) (2026-10-21)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **deps:** use @plutocms/pluto 0.17 ([#91](https://github.com/plutocms/supabase/issues/91)) ([7deeac5](https://github.com/plutocms/supabase/commit/7deeac5379ddaac0317fa457897190c0850e8ffc))
9
+ * **typegen:** keep the PostgREST version block and the app's format ([#90](https://github.com/plutocms/supabase/issues/90)) ([2518c33](https://github.com/plutocms/supabase/commit/2518c33f69fe188e69d93695819d7d3ca19932d7))
10
+
11
+
12
+ ### Performance Improvements
13
+
14
+ * **admin:** load the admin components on demand and start the setup check early ([#89](https://github.com/plutocms/supabase/issues/89)) ([f0d36bc](https://github.com/plutocms/supabase/commit/f0d36bc7d46f911522990653cd93ebb523c5a774))
15
+
16
+ ## [0.11.0](https://github.com/plutocms/supabase/compare/v0.10.0...v0.11.0) (2026-10-20)
17
+
18
+
19
+ ### ⚠ BREAKING CHANGES
20
+
21
+ * **admin:** move the admin shell to the pluto auth driver ([#83](https://github.com/plutocms/supabase/issues/83))
22
+
23
+ ### Features
24
+
25
+ * **admin:** move the admin shell to the pluto auth driver ([#83](https://github.com/plutocms/supabase/issues/83)) ([52ba78f](https://github.com/plutocms/supabase/commit/52ba78f934255e70a7b5188982bdb83f9fa5f86a))
26
+
3
27
  ## [0.10.0](https://github.com/plutocms/supabase/compare/v0.9.1...v0.10.0) (2026-10-18)
4
28
 
5
29
 
package/FEATURES.md CHANGED
@@ -13,6 +13,9 @@ policies shared by every project here.
13
13
  - Content adapter: a Supabase/PostgREST-backed `PlutoContentAdapter`, plugging any layer's
14
14
  declared content type into `@plutocms/pluto`'s generic content routes.
15
15
  @.claude/skills/content-adapter/SKILL.md
16
+ - Auth driver: plugs Supabase sign-in into the admin shell that pluto core owns (navbar, sign-in
17
+ page, route guard), and keeps the setup guard in the driver.
18
+ @.claude/skills/auth-driver/SKILL.md
16
19
  - Profile language: stores each user's admin language in `profiles.locale`, written through a
17
20
  security-definer function, and plugs it into pluto's i18n as a locale driver.
18
21
  @.claude/skills/profile-language/SKILL.md
package/README.md CHANGED
@@ -45,7 +45,16 @@ export default defineNuxtConfig({
45
45
 
46
46
  This layer provides the following composables:
47
47
 
48
- - `useAuth`: Manage user authentication with Supabase.
48
+ - `useAuth`: Sign in, request a password reset, and update the password, with Supabase.
49
+
50
+ ### Admin shell
51
+
52
+ Pluto core owns the admin shell: the `/admin` route, the sign-in page, the route guard and the
53
+ top navbar. This layer registers an auth driver, and core renders the shell from it. See
54
+ `.claude/skills/auth-driver/SKILL.md`.
55
+
56
+ An app that overrode `NavbarAdmin` or `pages/admin.vue` of an older version must move to registry
57
+ entries. See "Migrating an app" in that skill.
49
58
 
50
59
  ### API Routes
51
60
 
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ // The email and password form of the supabase layer. It is the `signIn`
3
+ // component of the auth driver. Core renders it inside the login frame
4
+ // (PlutoAdminAuthCard), so this file has no frame. The file lives under
5
+ // `app/` so Tailwind scans it.
6
+ const props = defineProps<{
7
+ /** A safe local path to open after sign-in. Core checks it. */
8
+ redirect: string
9
+ }>()
10
+
11
+ const { t } = usePlutoI18n()
12
+
13
+ const { isSubmitting, login } = useAuth({ redirect: () => props.redirect })
14
+
15
+ const form = ref({
16
+ email: '',
17
+ password: '',
18
+ })
19
+
20
+ function submitForm() {
21
+ login(form.value)
22
+ }
23
+ </script>
24
+
25
+ <template>
26
+ <form @submit.prevent="submitForm">
27
+ <div class="flex flex-col gap-y-6">
28
+ <UFormField :label="t('supabase.auth.fields.email')">
29
+ <UInput
30
+ v-model="form.email"
31
+ placeholder="victor@example.com"
32
+ class="w-full"
33
+ autocomplete="email"
34
+ />
35
+ </UFormField>
36
+
37
+ <UFormField :label="t('supabase.auth.fields.password')">
38
+ <UInput
39
+ v-model="form.password"
40
+ placeholder="••••"
41
+ class="w-full"
42
+ type="password"
43
+ />
44
+ </UFormField>
45
+
46
+ <UButton
47
+ :loading="isSubmitting"
48
+ type="submit"
49
+ icon="lucide:log-in"
50
+ class="w-full justify-center"
51
+ >
52
+ {{ t('supabase.auth.login.submit') }}
53
+ </UButton>
54
+ </div>
55
+ </form>
56
+ </template>
@@ -1,25 +1,39 @@
1
1
  <script setup lang="ts">
2
+ // The pending-migrations notice. It is an `adminBanners` entry of this layer,
3
+ // so the core admin layout renders it at the top of the admin pages.
2
4
  // Renders nothing when there is no logged-in admin (status reads as null)
3
5
  // or when no layer is pending. See `useMigrations` for the 401/403 handling.
6
+ // It also hides on the migrations page itself.
7
+ const route = useRoute()
4
8
  const { status, pendingCount } = useMigrations()
5
9
  const { t } = usePlutoI18n()
6
10
 
7
11
  const title = computed(() =>
8
12
  t('supabase.migrations.banner', pendingCount.value)
9
13
  )
14
+
15
+ const isVisible = computed(
16
+ () =>
17
+ Boolean(status.value)
18
+ && pendingCount.value > 0
19
+ && !route.path.startsWith('/admin/migrations')
20
+ )
10
21
  </script>
11
22
 
12
23
  <template>
13
- <UAlert
14
- v-if="status && pendingCount > 0"
15
- :title="title"
16
- color="warning"
17
- variant="subtle"
18
- icon="lucide:triangle-alert"
19
- >
20
- <template #description>
21
- <ULink to="/admin/migrations" class="underline"> {{ t('supabase.migrations.goTo') }} </ULink
22
- >.
23
- </template>
24
- </UAlert>
24
+ <ClientOnly>
25
+ <Container v-if="isVisible" class="pt-4">
26
+ <UAlert
27
+ :title="title"
28
+ color="warning"
29
+ variant="subtle"
30
+ icon="lucide:triangle-alert"
31
+ >
32
+ <template #description>
33
+ <ULink to="/admin/migrations" class="underline"> {{ t('supabase.migrations.goTo') }} </ULink
34
+ >.
35
+ </template>
36
+ </UAlert>
37
+ </Container>
38
+ </ClientOnly>
25
39
  </template>
@@ -1,33 +1,17 @@
1
- import type {
2
- RouteLocationNormalizedGeneric,
3
- RouteLocationRaw,
4
- } from 'vue-router'
1
+ import type { MaybeRefOrGetter } from 'vue'
5
2
 
6
3
  interface PlutoSupabaseAuthOptions {
7
- route?: RouteLocationNormalizedGeneric
4
+ /**
5
+ * A safe local path to open after a sign-in. Core checks it with
6
+ * `safeRedirect`. When it is empty, the sign-in opens the admin home.
7
+ */
8
+ redirect?: MaybeRefOrGetter<string | undefined>
8
9
  }
9
10
 
10
- interface PlutoUserMetadata {
11
- display_name?: string
12
- full_name?: string
13
- name?: string
14
- username?: string
15
- is_admin?: boolean
16
- [key: string]: unknown
17
- }
18
-
19
- // Admin paths that a signed-out visitor may load. Keep this list in sync
20
- // with every public `/admin/*` page.
21
- export const allowedUnauthenticatedPaths: RouteLocationRaw[] = [
22
- '/admin/login',
23
- '/admin/signup',
24
- '/admin/confirm',
25
- '/admin/setup',
26
- '/admin/forgot-password',
27
- '/admin/update-password',
28
- ]
29
-
30
- export async function useAuth(authOptions?: PlutoSupabaseAuthOptions) {
11
+ // Sign-in, password reset and password update for the supabase auth pages.
12
+ // The session, the user and the sign-out are in the auth driver
13
+ // (app/plugins/pluto-extension.ts), not here.
14
+ export function useAuth(authOptions?: PlutoSupabaseAuthOptions) {
31
15
  const toast = useToast()
32
16
  // Read at setup, while the Nuxt context exists. The functions below run
33
17
  // later, after an await, when the context is gone.
@@ -36,34 +20,7 @@ export async function useAuth(authOptions?: PlutoSupabaseAuthOptions) {
36
20
  resolveAuthErrorText(error, { t, te })
37
21
 
38
22
  const supabase = useSupabaseClient()
39
- const supabaseSession = useSupabaseSession()
40
-
41
- const supabaseUser = useSupabaseUser()
42
- const userMetadata = useState<PlutoUserMetadata | null>(
43
- 'pluto-auth-user-metadata',
44
- () => null
45
- )
46
-
47
- const { data: authenticatedUser } = await supabase.auth.getUser()
48
23
 
49
- if (authenticatedUser.user?.user_metadata) {
50
- userMetadata.value = authenticatedUser.user.user_metadata
51
- }
52
-
53
- watch(
54
- supabaseUser,
55
- (currentUser) => {
56
- if (currentUser?.user_metadata) {
57
- userMetadata.value = currentUser.user_metadata
58
- }
59
- },
60
- { immediate: true }
61
- )
62
-
63
- const user = computed(
64
- () => supabaseUser.value?.user_metadata ?? userMetadata.value
65
- )
66
- const isLoggedIn = computed<boolean>(() => !!supabaseSession.value)
67
24
  const isSubmitting = ref<boolean>(false)
68
25
 
69
26
  interface LoginForm {
@@ -103,6 +60,17 @@ export async function useAuth(authOptions?: PlutoSupabaseAuthOptions) {
103
60
  .eq('config_name', 'first_setup')
104
61
 
105
62
  if (healthcheckError) {
63
+ console.error(`Error saving first setup: ${healthcheckError.message}`)
64
+
65
+ toast.add({
66
+ title: t('supabase.auth.toasts.loginFailed'),
67
+ description: authErrorText(healthcheckError),
68
+ icon: 'lucide:circle-x',
69
+ color: 'error',
70
+ })
71
+
72
+ isSubmitting.value = false
73
+
106
74
  return {
107
75
  success: false,
108
76
  message: healthcheckError.message,
@@ -112,7 +80,7 @@ export async function useAuth(authOptions?: PlutoSupabaseAuthOptions) {
112
80
  }
113
81
 
114
82
  return navigateTo(
115
- (authOptions?.route?.query.redirect as string) ?? '/admin/home'
83
+ toValue(authOptions?.redirect) || SUPABASE_SIGN_IN_FALLBACK
116
84
  )
117
85
  } catch (error) {
118
86
  if (import.meta.dev) {
@@ -125,36 +93,6 @@ export async function useAuth(authOptions?: PlutoSupabaseAuthOptions) {
125
93
  }
126
94
  }
127
95
 
128
- interface LogoutOptions {
129
- redirectTo?: string
130
- showToast?: boolean
131
- }
132
-
133
- async function logout(options?: LogoutOptions) {
134
- await supabase.auth.signOut()
135
- userMetadata.value = null
136
-
137
- if (options?.showToast) {
138
- console.warn(`User logged out`)
139
-
140
- toast.add({
141
- title: t('supabase.auth.toasts.loggedOut'),
142
- icon: 'lucide:check',
143
- color: 'success',
144
- })
145
- }
146
-
147
- if (options?.redirectTo) {
148
- return navigateTo(`/admin/login?redirect=${options.redirectTo}`)
149
- }
150
-
151
- if (authOptions?.route && authOptions.route.path.startsWith('/admin')) {
152
- return navigateTo('/admin/login')
153
- }
154
-
155
- return navigateTo('/admin/login')
156
- }
157
-
158
96
  async function requestPasswordReset(email: string) {
159
97
  isSubmitting.value = true
160
98
 
@@ -228,13 +166,9 @@ export async function useAuth(authOptions?: PlutoSupabaseAuthOptions) {
228
166
  }
229
167
 
230
168
  return {
231
- isLoggedIn,
232
169
  isSubmitting,
233
- user,
234
170
  login,
235
- logout,
236
171
  requestPasswordReset,
237
172
  updatePassword,
238
- allowedUnauthenticatedPaths,
239
173
  }
240
174
  }
@@ -11,29 +11,25 @@ useHead({
11
11
  </script>
12
12
 
13
13
  <template>
14
- <div class="grid min-h-dvh place-items-center p-4">
15
- <UCard class="w-full max-w-100">
16
- <div class="space-y-6 py-10">
17
- <div class="text-center space-y-4">
18
- <hgroup class="flex flex-col gap-y-1">
19
- <span class="text-5xl">
20
- <UIcon name="lucide:mail-check" class="inline-block" />
21
- </span>
14
+ <PlutoAdminAuthCard :title="t('supabase.auth.confirm.title')">
15
+ <div class="space-y-6 py-10">
16
+ <div class="text-center space-y-4">
17
+ <hgroup class="flex flex-col gap-y-1">
18
+ <span class="text-5xl">
19
+ <UIcon name="lucide:mail-check" class="inline-block" />
20
+ </span>
21
+ </hgroup>
22
22
 
23
- <h1 class="text-3xl font-bold">{{ t('supabase.auth.confirm.title') }}</h1>
24
- </hgroup>
23
+ <p>
24
+ {{ t('supabase.auth.confirm.description') }}
25
+ </p>
25
26
 
26
- <p>
27
- {{ t('supabase.auth.confirm.description') }}
28
- </p>
29
-
30
- <div class="flex items-center justify-center gap-x-4">
31
- <UButton to="/admin" color="primary" icon="lucide:layout-dashboard">
32
- {{ t('supabase.auth.confirm.goToDashboard') }}
33
- </UButton>
34
- </div>
27
+ <div class="flex items-center justify-center gap-x-4">
28
+ <UButton to="/admin" color="primary" icon="lucide:layout-dashboard">
29
+ {{ t('supabase.auth.confirm.goToDashboard') }}
30
+ </UButton>
35
31
  </div>
36
32
  </div>
37
- </UCard>
38
- </div>
33
+ </div>
34
+ </PlutoAdminAuthCard>
39
35
  </template>
@@ -9,7 +9,7 @@ useHead({
9
9
  title: () => t('supabase.auth.forgot.pageTitle'),
10
10
  })
11
11
 
12
- const { isSubmitting, requestPasswordReset } = await useAuth()
12
+ const { isSubmitting, requestPasswordReset } = useAuth()
13
13
 
14
14
  const email = ref('')
15
15
  const isEmailSent = ref(false)
@@ -24,76 +24,69 @@ async function submitForm() {
24
24
  </script>
25
25
 
26
26
  <template>
27
- <div class="grid min-h-dvh place-items-center p-4">
28
- <UCard class="w-full max-w-100">
29
- <div v-if="isEmailSent" class="space-y-6 py-10">
30
- <div class="text-center space-y-4">
31
- <hgroup class="flex flex-col gap-y-1">
32
- <span class="text-5xl">
33
- <UIcon name="lucide:mail" class="inline-block" />
34
- </span>
27
+ <PlutoAdminAuthCard
28
+ :title="isEmailSent ? t('supabase.auth.forgot.sentTitle') : t('supabase.auth.forgot.title')"
29
+ :description="isEmailSent ? undefined : t('supabase.auth.forgot.description')"
30
+ >
31
+ <div v-if="isEmailSent" class="space-y-6 py-10">
32
+ <div class="text-center space-y-4">
33
+ <hgroup class="flex flex-col gap-y-1">
34
+ <span class="text-5xl">
35
+ <UIcon name="lucide:mail" class="inline-block" />
36
+ </span>
37
+ </hgroup>
35
38
 
36
- <h1 class="text-3xl font-bold">{{ t('supabase.auth.forgot.sentTitle') }}</h1>
37
- </hgroup>
39
+ <p>
40
+ <SupabaseMessageParts :message="t('supabase.auth.forgot.sentDescription')">
41
+ <template #email>
42
+ <strong>{{ email }}</strong>
43
+ </template>
44
+ </SupabaseMessageParts>
45
+ </p>
38
46
 
39
- <p>
40
- <SupabaseMessageParts :message="t('supabase.auth.forgot.sentDescription')">
41
- <template #email>
42
- <strong>{{ email }}</strong>
43
- </template>
44
- </SupabaseMessageParts>
45
- </p>
46
-
47
- <div class="flex items-center justify-center">
48
- <UButton variant="link" to="/admin/login" class="px-0">
49
- {{ t('supabase.auth.backToLogin') }}
50
- </UButton>
51
- </div>
47
+ <div class="flex items-center justify-center">
48
+ <UButton variant="link" to="/admin/login" class="px-0">
49
+ {{ t('supabase.auth.backToLogin') }}
50
+ </UButton>
52
51
  </div>
53
52
  </div>
53
+ </div>
54
54
 
55
- <form v-else @submit.prevent="submitForm">
56
- <div class="flex flex-col gap-y-6">
57
- <hgroup class="flex flex-col gap-y-1">
58
- <h1 class="text-3xl font-bold">{{ t('supabase.auth.forgot.title') }}</h1>
59
- <p class="text-sm text-muted">
60
- {{ t('supabase.auth.forgot.description') }}
61
- </p>
62
- </hgroup>
55
+ <form v-else @submit.prevent="submitForm">
56
+ <div class="flex flex-col gap-y-6">
63
57
 
64
- <UFormField :label="t('supabase.auth.fields.email')">
65
- <UInput
66
- v-model="email"
67
- placeholder="victor@example.com"
68
- type="email"
69
- class="w-full"
70
- autocomplete="email"
71
- required
72
- />
73
- </UFormField>
58
+ <UFormField :label="t('supabase.auth.fields.email')">
59
+ <UInput
60
+ v-model="email"
61
+ placeholder="victor@example.com"
62
+ type="email"
63
+ class="w-full"
64
+ autocomplete="email"
65
+ required
66
+ />
67
+ </UFormField>
74
68
 
75
- <div
76
- class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-x-4"
77
- >
78
- <UFormField>
79
- <UButton
80
- :loading="isSubmitting"
81
- type="submit"
82
- icon="lucide:send"
83
- class="w-full justify-center sm:w-auto"
84
- >
85
- {{ t('supabase.auth.forgot.submit') }}
86
- </UButton>
87
- </UFormField>
69
+ <div
70
+ class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-x-4"
71
+ >
72
+ <UFormField>
73
+ <UButton
74
+ :loading="isSubmitting"
75
+ type="submit"
76
+ icon="lucide:send"
77
+ class="w-full justify-center sm:w-auto"
78
+ >
79
+ {{ t('supabase.auth.forgot.submit') }}
80
+ </UButton>
81
+ </UFormField>
88
82
 
89
- <UFormField>
90
- <UButton variant="link" to="/admin/login" class="px-0">
91
- {{ t('supabase.auth.backToLogin') }}
92
- </UButton>
93
- </UFormField>
94
- </div>
83
+ <UFormField>
84
+ <UButton variant="link" to="/admin/login" class="px-0">
85
+ {{ t('supabase.auth.backToLogin') }}
86
+ </UButton>
87
+ </UFormField>
95
88
  </div>
96
- </form>
97
- </UCard>
98
- </div>
89
+ </div>
90
+ </form>
91
+ </PlutoAdminAuthCard>
99
92
  </template>