@plutocms/supabase 0.9.0 → 0.10.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 (35) hide show
  1. package/.prettierignore +2 -0
  2. package/CHANGELOG.md +15 -0
  3. package/FEATURES.md +6 -0
  4. package/app/components/EnvPersistWarning.vue +30 -13
  5. package/app/components/SupabaseMessageParts.vue +29 -0
  6. package/app/components/migrations/MigrationsBanner.vue +3 -4
  7. package/app/components/navbar/NavbarAdmin.vue +15 -9
  8. package/app/components/settings/SettingsGeneralPanel.vue +21 -6
  9. package/app/composables/auth.ts +17 -14
  10. package/app/pages/admin/(auth)/confirm.vue +6 -5
  11. package/app/pages/admin/(auth)/forgot-password.vue +15 -11
  12. package/app/pages/admin/(auth)/login.vue +10 -8
  13. package/app/pages/admin/(auth)/signup.vue +30 -22
  14. package/app/pages/admin/(auth)/update-password.vue +15 -12
  15. package/app/pages/admin/migrations.vue +87 -51
  16. package/app/pages/admin/setup.vue +88 -74
  17. package/app/pages/admin.vue +3 -2
  18. package/app/plugins/pluto-extension.ts +24 -3
  19. package/app/plugins/pluto-locale-sync.client.ts +30 -0
  20. package/app/utils/auth-error.ts +53 -0
  21. package/db/migrations/005_profile_locale.sql +44 -0
  22. package/i18n/locales/en.json +209 -0
  23. package/i18n/locales/pt-BR.json +209 -0
  24. package/package.json +3 -3
  25. package/server/api/profile/locale.get.ts +39 -0
  26. package/server/api/profile/locale.post.ts +71 -0
  27. package/server/api/setup/create.post.ts +6 -4
  28. package/server/api/users/[id].get.ts +4 -2
  29. package/server/api/users/index.get.ts +4 -2
  30. package/server/utils/admin-guard.ts +10 -2
  31. package/server/utils/capability-guard.ts +18 -4
  32. package/server/utils/content-adapter.ts +6 -4
  33. package/server/utils/pending-migrations.ts +0 -0
  34. package/server/utils/validate.ts +6 -1
  35. package/shared/types/supabase.ts +7 -0
@@ -3,8 +3,11 @@ definePageMeta({
3
3
  layout: 'auth',
4
4
  })
5
5
 
6
+ const { t } = usePlutoI18n()
7
+ const errorText = usePlutoErrorText()
8
+
6
9
  useHead({
7
- title: 'Sign Up',
10
+ title: () => t('supabase.auth.signup.pageTitle'),
8
11
  })
9
12
 
10
13
  const toast = useToast()
@@ -44,8 +47,8 @@ async function submitForm() {
44
47
  })
45
48
 
46
49
  toast.add({
47
- title: 'Account created successfully',
48
- description: 'You still need to verify your email before you can log in.',
50
+ title: t('supabase.auth.signup.created'),
51
+ description: t('supabase.auth.signup.createdDescription'),
49
52
  icon: 'lucide:check',
50
53
  color: 'success',
51
54
  })
@@ -55,9 +58,8 @@ async function submitForm() {
55
58
  console.error('Error signing up:', error)
56
59
 
57
60
  toast.add({
58
- title: 'Could not create account',
59
- description:
60
- error instanceof Error ? error.message : 'An unknown error occurred.',
61
+ title: t('supabase.auth.signup.failed'),
62
+ description: errorText(error, 'supabase.auth.errors.unknown'),
61
63
  icon: 'lucide:circle-x',
62
64
  color: 'error',
63
65
  })
@@ -77,18 +79,16 @@ async function submitForm() {
77
79
  <UIcon name="lucide:mail-warning" class="inline-block" />
78
80
  </span>
79
81
 
80
- <h1 class="text-3xl font-bold">Almost there!</h1>
82
+ <h1 class="text-3xl font-bold">{{ t('supabase.auth.signup.almostThere') }}</h1>
81
83
  </hgroup>
82
84
 
83
85
  <p>
84
- A verification link has been sent to your email. Please check your
85
- inbox and follow the instructions to verify your account.
86
+ {{ t('supabase.auth.signup.verificationSent') }}
86
87
  </p>
87
88
 
88
89
  <UAlert icon="lucide:check" color="success" variant="outline">
89
90
  <template #description>
90
- You can close this page. You will be logged in automatically once
91
- you verify your email.
91
+ {{ t('supabase.auth.signup.closePage') }}
92
92
  </template>
93
93
  </UAlert>
94
94
 
@@ -98,7 +98,7 @@ async function submitForm() {
98
98
  variant="outline"
99
99
  >
100
100
  <template #description>
101
- If you don't see the email, please check your spam folder.
101
+ {{ t('supabase.auth.signup.checkSpam') }}
102
102
  </template>
103
103
  </UAlert>
104
104
  </div>
@@ -106,9 +106,9 @@ async function submitForm() {
106
106
 
107
107
  <form v-else @submit.prevent="submitForm">
108
108
  <div class="flex flex-col gap-y-6">
109
- <h1 class="text-3xl font-bold">Create Account</h1>
109
+ <h1 class="text-3xl font-bold">{{ t('supabase.auth.signup.title') }}</h1>
110
110
 
111
- <UFormField label="Email" required>
111
+ <UFormField :label="t('supabase.auth.fields.email')" required>
112
112
  <UInput
113
113
  v-model="form.email"
114
114
  type="email"
@@ -119,27 +119,31 @@ async function submitForm() {
119
119
  </UFormField>
120
120
 
121
121
  <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
122
- <UFormField label="Username" required>
122
+ <UFormField :label="t('supabase.auth.fields.username')" required>
123
123
  <UInput
124
124
  v-model="form.username"
125
- placeholder="e.g: johndoe"
125
+ :placeholder="t('supabase.auth.signup.usernamePlaceholder')"
126
126
  class="w-full"
127
127
  minlength="3"
128
128
  required
129
129
  />
130
130
  </UFormField>
131
131
 
132
- <UFormField label="Display Name" required>
132
+ <UFormField :label="t('supabase.auth.fields.displayName')" required>
133
133
  <UInput
134
134
  v-model="form.display_name"
135
- placeholder="e.g: John Doe"
135
+ :placeholder="t('supabase.auth.signup.displayNamePlaceholder')"
136
136
  class="w-full"
137
137
  required
138
138
  />
139
139
  </UFormField>
140
140
  </div>
141
141
 
142
- <UFormField :error="!passwordMatch" label="Password" required>
142
+ <UFormField
143
+ :error="!passwordMatch"
144
+ :label="t('supabase.auth.fields.password')"
145
+ required
146
+ >
143
147
  <UInput
144
148
  v-model="form.password"
145
149
  placeholder="••••"
@@ -149,7 +153,11 @@ async function submitForm() {
149
153
  />
150
154
  </UFormField>
151
155
 
152
- <UFormField :error="!passwordMatch" label="Confirm Password" required>
156
+ <UFormField
157
+ :error="!passwordMatch"
158
+ :label="t('supabase.auth.fields.confirmPassword')"
159
+ required
160
+ >
153
161
  <UInput
154
162
  v-model="form.confirm_password"
155
163
  placeholder="••••"
@@ -169,7 +177,7 @@ async function submitForm() {
169
177
  variant="link"
170
178
  class="px-0"
171
179
  >
172
- Back to Login
180
+ {{ t('supabase.auth.backToLogin') }}
173
181
  </UButton>
174
182
  </UFormField>
175
183
 
@@ -180,7 +188,7 @@ async function submitForm() {
180
188
  type="submit"
181
189
  class="w-full justify-center sm:w-auto"
182
190
  >
183
- Create account
191
+ {{ t('supabase.auth.signup.submit') }}
184
192
  </UButton>
185
193
  </UFormField>
186
194
  </div>
@@ -3,8 +3,10 @@ definePageMeta({
3
3
  layout: 'auth',
4
4
  })
5
5
 
6
+ const { t } = usePlutoI18n()
7
+
6
8
  useHead({
7
- title: 'Update Password',
9
+ title: () => t('supabase.auth.update.title'),
8
10
  })
9
11
 
10
12
  const route = useRoute()
@@ -57,17 +59,16 @@ async function submitForm() {
57
59
  <UIcon name="lucide:lock-keyhole" class="inline-block" />
58
60
  </span>
59
61
 
60
- <h1 class="text-3xl font-bold">Password Updated!</h1>
62
+ <h1 class="text-3xl font-bold">{{ t('supabase.auth.update.doneTitle') }}</h1>
61
63
  </hgroup>
62
64
 
63
65
  <p>
64
- Your password has been changed. You can now log in with your new
65
- password.
66
+ {{ t('supabase.auth.update.doneDescription') }}
66
67
  </p>
67
68
 
68
69
  <div class="flex items-center justify-center">
69
70
  <UButton to="/admin/login" color="primary" icon="lucide:log-in">
70
- Go to Login
71
+ {{ t('supabase.auth.update.goToLogin') }}
71
72
  </UButton>
72
73
  </div>
73
74
  </div>
@@ -76,11 +77,13 @@ async function submitForm() {
76
77
  <form v-else @submit.prevent="submitForm">
77
78
  <div class="flex flex-col gap-y-6">
78
79
  <hgroup class="flex flex-col gap-y-1">
79
- <h1 class="text-3xl font-bold">Update Password</h1>
80
- <p class="text-sm text-muted">Enter your new password below.</p>
80
+ <h1 class="text-3xl font-bold">{{ t('supabase.auth.update.title') }}</h1>
81
+ <p class="text-sm text-muted">
82
+ {{ t('supabase.auth.update.description') }}
83
+ </p>
81
84
  </hgroup>
82
85
 
83
- <UFormField label="New Password">
86
+ <UFormField :label="t('supabase.auth.update.newPassword')">
84
87
  <UInput
85
88
  v-model="form.password"
86
89
  placeholder="••••"
@@ -94,10 +97,10 @@ async function submitForm() {
94
97
  <UFormField
95
98
  :error="
96
99
  form.confirm_password && !passwordMatch
97
- ? 'Passwords do not match.'
100
+ ? t('supabase.auth.passwordMismatch')
98
101
  : undefined
99
102
  "
100
- label="Confirm New Password"
103
+ :label="t('supabase.auth.update.confirmNewPassword')"
101
104
  >
102
105
  <UInput
103
106
  v-model="form.confirm_password"
@@ -120,13 +123,13 @@ async function submitForm() {
120
123
  icon="lucide:lock-keyhole"
121
124
  class="w-full justify-center sm:w-auto"
122
125
  >
123
- Update Password
126
+ {{ t('supabase.auth.update.submit') }}
124
127
  </UButton>
125
128
  </UFormField>
126
129
 
127
130
  <UFormField>
128
131
  <UButton variant="link" to="/admin/login" class="px-0">
129
- Back to Login
132
+ {{ t('supabase.auth.backToLogin') }}
130
133
  </UButton>
131
134
  </UFormField>
132
135
  </div>
@@ -1,6 +1,8 @@
1
1
  <script setup lang="ts">
2
+ const { t } = usePlutoI18n()
3
+
2
4
  useHead({
3
- title: 'Migrations',
5
+ title: () => t('supabase.migrations.title'),
4
6
  })
5
7
 
6
8
  const config = useRuntimeConfig()
@@ -15,6 +17,19 @@ const { status, fetchStatus, pendingCount, refresh, runMigrations } =
15
17
 
16
18
  const isDev = import.meta.dev
17
19
 
20
+ // The text of each result status that the server returns.
21
+ const statusLabels: Record<string, string> = {
22
+ applied: 'supabase.migrations.status.applied',
23
+ skipped: 'supabase.migrations.status.skipped',
24
+ failed: 'supabase.migrations.status.failed',
25
+ }
26
+
27
+ function statusText(status: string): string {
28
+ const key = statusLabels[status]
29
+
30
+ return key ? t(key) : status
31
+ }
32
+
18
33
  // Only the layers that actually have a pending, or applied, file — used to
19
34
  // render per-file detail under each layer below.
20
35
  const pendingLayers = computed(
@@ -86,9 +101,9 @@ async function applyMigrations(useForm: boolean) {
86
101
 
87
102
  if (result.success === false) {
88
103
  toast.add({
89
- title: 'Could not apply migrations',
104
+ title: t('supabase.migrations.applyFailed'),
90
105
  description: result.needsConnectionString
91
- ? 'A database connection string is required.'
106
+ ? t('supabase.migrations.connectionRequired')
92
107
  : undefined,
93
108
  icon: 'lucide:circle-x',
94
109
  color: 'error',
@@ -98,7 +113,7 @@ async function applyMigrations(useForm: boolean) {
98
113
  }
99
114
 
100
115
  toast.add({
101
- title: 'Migrations applied',
116
+ title: t('supabase.migrations.applied'),
102
117
  icon: 'lucide:check-circle',
103
118
  color: 'success',
104
119
  })
@@ -118,9 +133,8 @@ async function applyMigrations(useForm: boolean) {
118
133
 
119
134
  if (isDev && useForm && !hasStatusCode) {
120
135
  toast.add({
121
- title: 'Dev server restarting',
122
- description:
123
- 'Saving a new connection string restarts the dev server. Reconnecting…',
136
+ title: t('supabase.connection.restarting'),
137
+ description: t('supabase.migrations.restartingDescription'),
124
138
  icon: 'lucide:refresh-cw',
125
139
  color: 'info',
126
140
  })
@@ -134,18 +148,21 @@ async function applyMigrations(useForm: boolean) {
134
148
  toast.add(
135
149
  recovered
136
150
  ? {
137
- title: 'Reconnected',
151
+ title: t('supabase.connection.reconnected'),
138
152
  description:
139
153
  pendingCount.value === 0
140
- ? 'The server is back. All migrations were applied.'
141
- : `The server is back. ${pendingCount.value} layer(s) still pending — check above.`,
154
+ ? t('supabase.migrations.reconnectedAllApplied')
155
+ : t('supabase.migrations.reconnectedPending', {
156
+ count: pendingCount.value,
157
+ }),
142
158
  icon: 'lucide:check-circle',
143
159
  color: 'success',
144
160
  }
145
161
  : {
146
- title: 'Still reconnecting',
147
- description:
148
- 'The dev server is taking longer than expected. Reload this page in a moment.',
162
+ title: t('supabase.connection.stillReconnecting'),
163
+ description: t(
164
+ 'supabase.connection.stillReconnectingDescription'
165
+ ),
149
166
  icon: 'lucide:triangle-alert',
150
167
  color: 'warning',
151
168
  }
@@ -159,8 +176,8 @@ async function applyMigrations(useForm: boolean) {
159
176
  }
160
177
 
161
178
  toast.add({
162
- title: 'Error applying migrations',
163
- description: 'Please check the console for more details.',
179
+ title: t('supabase.migrations.error'),
180
+ description: t('supabase.connection.checkConsole'),
164
181
  icon: 'lucide:circle-x',
165
182
  color: 'error',
166
183
  })
@@ -172,15 +189,15 @@ async function applyMigrations(useForm: boolean) {
172
189
 
173
190
  <template>
174
191
  <AdminView>
175
- <h1 class="text-3xl font-bold lg:text-4xl">Migrations</h1>
192
+ <h1 class="text-3xl font-bold lg:text-4xl">{{ t('supabase.migrations.title') }}</h1>
176
193
 
177
194
  <UAlert
178
195
  v-if="isReconnecting"
196
+ :title="t('supabase.connection.restarting')"
197
+ :description="t('supabase.migrations.reconnectingBanner')"
179
198
  color="info"
180
199
  variant="outline"
181
200
  icon="lucide:refresh-cw"
182
- title="Dev server restarting"
183
- description="Saving a new connection string restarts the dev server. Reconnecting — this page will update on its own once it's back."
184
201
  />
185
202
 
186
203
  <div
@@ -188,7 +205,7 @@ async function applyMigrations(useForm: boolean) {
188
205
  class="flex items-center gap-x-2"
189
206
  >
190
207
  <Icon name="svg-spinners:ring-resize" />
191
- <span>Loading migration status…</span>
208
+ <span>{{ t('supabase.migrations.loading') }}</span>
192
209
  </div>
193
210
 
194
211
  <template v-else-if="status">
@@ -196,16 +213,20 @@ async function applyMigrations(useForm: boolean) {
196
213
  <div class="flex flex-col gap-y-6">
197
214
  <UAlert
198
215
  v-if="pendingCount === 0"
216
+ :title="t('supabase.migrations.upToDate')"
217
+ :description="t('supabase.migrations.upToDateDescription')"
199
218
  color="success"
200
219
  variant="outline"
201
220
  icon="lucide:check-circle"
202
- title="Everything is up to date"
203
- description="No layer needs a database migration."
204
221
  />
205
222
 
206
223
  <div v-else class="flex flex-col gap-y-2">
207
224
  <h2 class="font-semibold">
208
- Pending layers ({{ status.pending.length }})
225
+ {{
226
+ t('supabase.migrations.pendingLayers', {
227
+ count: status.pending.length,
228
+ })
229
+ }}
209
230
  </h2>
210
231
  <ul class="flex flex-col gap-y-2 text-sm">
211
232
  <li v-for="layer in pendingLayers" :key="layer.layerName">
@@ -221,7 +242,11 @@ async function applyMigrations(useForm: boolean) {
221
242
 
222
243
  <div v-if="status.applied.length" class="flex flex-col gap-y-2">
223
244
  <h2 class="font-semibold">
224
- Applied layers ({{ status.applied.length }})
245
+ {{
246
+ t('supabase.migrations.appliedLayers', {
247
+ count: status.applied.length,
248
+ })
249
+ }}
225
250
  </h2>
226
251
  <ul class="flex flex-col gap-y-2 text-sm">
227
252
  <li v-for="layer in appliedLayers" :key="layer.layerName">
@@ -241,7 +266,7 @@ async function applyMigrations(useForm: boolean) {
241
266
  class="flex flex-col gap-y-4"
242
267
  @submit="applyMigrations(true)"
243
268
  >
244
- <UFormField label="Supabase connection string" required>
269
+ <UFormField :label="t('supabase.connection.connectionString')" required>
245
270
  <UInput
246
271
  v-model="connectionForm.connectionString"
247
272
  :disabled="isRunning"
@@ -250,19 +275,24 @@ async function applyMigrations(useForm: boolean) {
250
275
  />
251
276
 
252
277
  <template #help>
253
- You can find your connection string
254
- <ULink
255
- :to="`https://supabase.com/dashboard/project/${projectId}/database/settings?showConnect=true&connectTab=direct&method=transaction`"
256
- target="_blank"
257
- class="underline"
258
- external
278
+ <SupabaseMessageParts
279
+ :message="t('supabase.connection.connectionStringHelp')"
259
280
  >
260
- clicking here </ULink
261
- >.
281
+ <template #link>
282
+ <ULink
283
+ :to="`https://supabase.com/dashboard/project/${projectId}/database/settings?showConnect=true&connectTab=direct&method=transaction`"
284
+ target="_blank"
285
+ class="underline"
286
+ external
287
+ >
288
+ {{ t('supabase.connection.connectionStringHelpLink') }}
289
+ </ULink>
290
+ </template>
291
+ </SupabaseMessageParts>
262
292
  </template>
263
293
  </UFormField>
264
294
 
265
- <UFormField label="Database password" required>
295
+ <UFormField :label="t('supabase.connection.password')" required>
266
296
  <UInput
267
297
  v-model="connectionForm.password"
268
298
  :disabled="isRunning"
@@ -278,32 +308,36 @@ async function applyMigrations(useForm: boolean) {
278
308
  variant="outline"
279
309
  >
280
310
  <template #description>
281
- Your password is created when you create your Supabase
282
- project. You can change it in the
283
- <ULink
284
- to="https://supabase.com/dashboard/project/_/database/settings"
285
- target="_blank"
311
+ <SupabaseMessageParts
312
+ :message="t('supabase.connection.passwordInfo')"
286
313
  >
287
- Supabase Database Settings </ULink
288
- >.
314
+ <template #link>
315
+ <ULink
316
+ to="https://supabase.com/dashboard/project/_/database/settings"
317
+ target="_blank"
318
+ >
319
+ {{ t('supabase.connection.passwordInfoLink') }}
320
+ </ULink>
321
+ </template>
322
+ </SupabaseMessageParts>
289
323
  </template>
290
324
  </UAlert>
291
325
 
292
326
  <UAlert
327
+ :title="t('supabase.connection.whyTitle')"
328
+ :description="t('supabase.migrations.why')"
293
329
  color="warning"
294
330
  variant="outline"
295
- title="Why do I need to provide a database password?"
296
331
  icon="lucide:info"
297
- description="It is used once to open a direct PostgreSQL connection and run the pending layer schemas. It is then written to .env on the server, so future migrations need no prompt. It is never stored anywhere else, and never sent back to your browser."
298
332
  />
299
333
 
300
334
  <UAlert
301
335
  v-if="isDev"
336
+ :title="t('supabase.connection.restartTitle')"
337
+ :description="t('supabase.migrations.restartDescription')"
302
338
  color="info"
303
339
  variant="outline"
304
- title="This restarts the dev server"
305
340
  icon="lucide:refresh-cw"
306
- description="Saving a new connection string writes it to .env. In development, that restarts the server automatically. The page will briefly show a reconnecting message — this is expected, not an error."
307
341
  />
308
342
 
309
343
  <div class="flex">
@@ -314,7 +348,7 @@ async function applyMigrations(useForm: boolean) {
314
348
  icon="lucide:database"
315
349
  class="w-full justify-center sm:w-auto"
316
350
  >
317
- Apply pending migrations
351
+ {{ t('supabase.migrations.apply') }}
318
352
  </UButton>
319
353
  </div>
320
354
  </UForm>
@@ -327,7 +361,7 @@ async function applyMigrations(useForm: boolean) {
327
361
  class="w-full justify-center sm:w-auto"
328
362
  @click="applyMigrations(false)"
329
363
  >
330
- Apply pending migrations
364
+ {{ t('supabase.migrations.apply') }}
331
365
  </UButton>
332
366
  </div>
333
367
  </template>
@@ -336,7 +370,9 @@ async function applyMigrations(useForm: boolean) {
336
370
 
337
371
  <UCard v-if="lastRun">
338
372
  <div class="flex flex-col gap-y-4">
339
- <h2 class="font-semibold">Last run</h2>
373
+ <h2 class="font-semibold">
374
+ {{ t('supabase.migrations.lastRun') }}
375
+ </h2>
340
376
 
341
377
  <EnvPersistWarning
342
378
  v-if="lastRun.success && lastRun.persisted === false"
@@ -354,7 +390,7 @@ async function applyMigrations(useForm: boolean) {
354
390
  <span class="font-mono"
355
391
  >{{ result.layerName }}/{{ result.migrationName }}</span
356
392
  >
357
- — {{ result.status }}
393
+ — {{ statusText(result.status) }}
358
394
  <span v-if="result.error" class="text-error">
359
395
  : {{ result.error }}</span
360
396
  >
@@ -366,11 +402,11 @@ async function applyMigrations(useForm: boolean) {
366
402
 
367
403
  <UAlert
368
404
  v-else
405
+ :title="t('supabase.migrations.loadFailed')"
406
+ :description="t('supabase.migrations.loadFailedDescription')"
369
407
  color="error"
370
408
  variant="outline"
371
409
  icon="lucide:circle-x"
372
- title="Could not load migration status"
373
- description="This isn't the dev-server-restart case above — something else went wrong reading migration status. Check the server console, then try again."
374
410
  >
375
411
  <template #actions>
376
412
  <UButton
@@ -379,7 +415,7 @@ async function applyMigrations(useForm: boolean) {
379
415
  icon="lucide:refresh-cw"
380
416
  @click="refresh()"
381
417
  >
382
- Retry
418
+ {{ t('supabase.migrations.retry') }}
383
419
  </UButton>
384
420
  </template>
385
421
  </UAlert>