@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.
- package/.prettierignore +2 -0
- package/CHANGELOG.md +15 -0
- package/FEATURES.md +6 -0
- package/app/components/EnvPersistWarning.vue +30 -13
- package/app/components/SupabaseMessageParts.vue +29 -0
- package/app/components/migrations/MigrationsBanner.vue +3 -4
- package/app/components/navbar/NavbarAdmin.vue +15 -9
- package/app/components/settings/SettingsGeneralPanel.vue +21 -6
- package/app/composables/auth.ts +17 -14
- package/app/pages/admin/(auth)/confirm.vue +6 -5
- package/app/pages/admin/(auth)/forgot-password.vue +15 -11
- package/app/pages/admin/(auth)/login.vue +10 -8
- package/app/pages/admin/(auth)/signup.vue +30 -22
- package/app/pages/admin/(auth)/update-password.vue +15 -12
- package/app/pages/admin/migrations.vue +87 -51
- package/app/pages/admin/setup.vue +88 -74
- package/app/pages/admin.vue +3 -2
- package/app/plugins/pluto-extension.ts +24 -3
- package/app/plugins/pluto-locale-sync.client.ts +30 -0
- package/app/utils/auth-error.ts +53 -0
- package/db/migrations/005_profile_locale.sql +44 -0
- package/i18n/locales/en.json +209 -0
- package/i18n/locales/pt-BR.json +209 -0
- package/package.json +3 -3
- package/server/api/profile/locale.get.ts +39 -0
- package/server/api/profile/locale.post.ts +71 -0
- package/server/api/setup/create.post.ts +6 -4
- package/server/api/users/[id].get.ts +4 -2
- package/server/api/users/index.get.ts +4 -2
- package/server/utils/admin-guard.ts +10 -2
- package/server/utils/capability-guard.ts +18 -4
- package/server/utils/content-adapter.ts +6 -4
- package/server/utils/pending-migrations.ts +0 -0
- package/server/utils/validate.ts +6 -1
- package/shared/types/supabase.ts +7 -0
|
@@ -6,8 +6,10 @@ definePageMeta({
|
|
|
6
6
|
layout: 'setup',
|
|
7
7
|
})
|
|
8
8
|
|
|
9
|
+
const { t, uiLocale } = usePlutoI18n()
|
|
10
|
+
|
|
9
11
|
useHead({
|
|
10
|
-
title:
|
|
12
|
+
title: () => t('supabase.setup.title'),
|
|
11
13
|
})
|
|
12
14
|
|
|
13
15
|
const config = useRuntimeConfig()
|
|
@@ -19,14 +21,14 @@ const toast = useToast()
|
|
|
19
21
|
|
|
20
22
|
const isDev = import.meta.dev
|
|
21
23
|
|
|
22
|
-
const items =
|
|
24
|
+
const items = computed<StepperItem[]>(() => [
|
|
23
25
|
{
|
|
24
|
-
title: '
|
|
26
|
+
title: t('supabase.setup.steps.database'),
|
|
25
27
|
icon: 'i-lucide-database',
|
|
26
28
|
},
|
|
27
29
|
|
|
28
30
|
{
|
|
29
|
-
title: '
|
|
31
|
+
title: t('supabase.setup.steps.complete'),
|
|
30
32
|
icon: 'i-lucide-check',
|
|
31
33
|
},
|
|
32
34
|
])
|
|
@@ -66,10 +68,10 @@ const currentStepId = computed(() => {
|
|
|
66
68
|
|
|
67
69
|
const currentStepSubmitLabel = computed(() => {
|
|
68
70
|
if (currentStep.value === 0) {
|
|
69
|
-
return '
|
|
71
|
+
return t('supabase.setup.submit')
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
return '
|
|
74
|
+
return t('supabase.setup.next')
|
|
73
75
|
})
|
|
74
76
|
|
|
75
77
|
/**
|
|
@@ -91,8 +93,8 @@ async function waitForServerAndAdvance(): Promise<boolean> {
|
|
|
91
93
|
await $fetch('/api/settings/first_setup')
|
|
92
94
|
|
|
93
95
|
toast.add({
|
|
94
|
-
title: '
|
|
95
|
-
description: '
|
|
96
|
+
title: t('supabase.connection.reconnected'),
|
|
97
|
+
description: t('supabase.setup.reconnectedDescription'),
|
|
96
98
|
icon: 'lucide:check-circle',
|
|
97
99
|
color: 'success',
|
|
98
100
|
})
|
|
@@ -126,8 +128,8 @@ async function completeDatabaseSetup() {
|
|
|
126
128
|
|
|
127
129
|
if (data.success === false) {
|
|
128
130
|
toast.add({
|
|
129
|
-
title: '
|
|
130
|
-
description: '
|
|
131
|
+
title: t('supabase.setup.failed'),
|
|
132
|
+
description: t('supabase.setup.failedDescription'),
|
|
131
133
|
icon: 'lucide:circle-x',
|
|
132
134
|
color: 'error',
|
|
133
135
|
})
|
|
@@ -141,8 +143,8 @@ async function completeDatabaseSetup() {
|
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
toast.add({
|
|
144
|
-
title: '
|
|
145
|
-
description: '
|
|
146
|
+
title: t('supabase.setup.complete'),
|
|
147
|
+
description: t('supabase.setup.completeDescription'),
|
|
146
148
|
icon: 'lucide:check-circle',
|
|
147
149
|
color: 'success',
|
|
148
150
|
})
|
|
@@ -164,8 +166,8 @@ async function completeDatabaseSetup() {
|
|
|
164
166
|
|
|
165
167
|
if (import.meta.dev && !hasStatusCode) {
|
|
166
168
|
toast.add({
|
|
167
|
-
title: '
|
|
168
|
-
description: '
|
|
169
|
+
title: t('supabase.connection.restarting'),
|
|
170
|
+
description: t('supabase.setup.restartingDescription'),
|
|
169
171
|
icon: 'lucide:refresh-cw',
|
|
170
172
|
color: 'info',
|
|
171
173
|
})
|
|
@@ -177,9 +179,8 @@ async function completeDatabaseSetup() {
|
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
toast.add({
|
|
180
|
-
title: '
|
|
181
|
-
description:
|
|
182
|
-
'The dev server is taking longer than expected. Reload this page in a moment.',
|
|
182
|
+
title: t('supabase.connection.stillReconnecting'),
|
|
183
|
+
description: t('supabase.connection.stillReconnectingDescription'),
|
|
183
184
|
icon: 'lucide:triangle-alert',
|
|
184
185
|
color: 'warning',
|
|
185
186
|
})
|
|
@@ -192,8 +193,8 @@ async function completeDatabaseSetup() {
|
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
toast.add({
|
|
195
|
-
title: '
|
|
196
|
-
description: '
|
|
196
|
+
title: t('supabase.setup.error'),
|
|
197
|
+
description: t('supabase.connection.checkConsole'),
|
|
197
198
|
icon: 'lucide:circle-x',
|
|
198
199
|
color: 'error',
|
|
199
200
|
})
|
|
@@ -210,7 +211,7 @@ function handleStepChange(step: number) {
|
|
|
210
211
|
</script>
|
|
211
212
|
|
|
212
213
|
<template>
|
|
213
|
-
<UApp>
|
|
214
|
+
<UApp :locale="uiLocale">
|
|
214
215
|
<div class="flex min-h-dvh items-center justify-center p-4">
|
|
215
216
|
<UCard class="w-full max-w-125">
|
|
216
217
|
<div class="flex flex-col gap-y-8">
|
|
@@ -223,7 +224,7 @@ function handleStepChange(step: number) {
|
|
|
223
224
|
class="flex flex-col gap-y-4"
|
|
224
225
|
@submit="handleStepChange(currentStep)"
|
|
225
226
|
>
|
|
226
|
-
<UFormField label="
|
|
227
|
+
<UFormField :label="t('supabase.connection.connectionString')" required>
|
|
227
228
|
<UInput
|
|
228
229
|
v-model="databaseForm.connectionString"
|
|
229
230
|
:disabled="isSettingUpDatabase"
|
|
@@ -233,19 +234,24 @@ function handleStepChange(step: number) {
|
|
|
233
234
|
/>
|
|
234
235
|
|
|
235
236
|
<template #help>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
:to="`https://supabase.com/dashboard/project/${projectId}/database/settings?showConnect=true&method=transaction`"
|
|
239
|
-
target="_blank"
|
|
240
|
-
class="underline"
|
|
241
|
-
external
|
|
237
|
+
<SupabaseMessageParts
|
|
238
|
+
:message="t('supabase.connection.connectionStringHelp')"
|
|
242
239
|
>
|
|
243
|
-
|
|
244
|
-
|
|
240
|
+
<template #link>
|
|
241
|
+
<ULink
|
|
242
|
+
:to="`https://supabase.com/dashboard/project/${projectId}/database/settings?showConnect=true&method=transaction`"
|
|
243
|
+
target="_blank"
|
|
244
|
+
class="underline"
|
|
245
|
+
external
|
|
246
|
+
>
|
|
247
|
+
{{ t('supabase.connection.connectionStringHelpLink') }}
|
|
248
|
+
</ULink>
|
|
249
|
+
</template>
|
|
250
|
+
</SupabaseMessageParts>
|
|
245
251
|
</template>
|
|
246
252
|
</UFormField>
|
|
247
253
|
|
|
248
|
-
<UFormField label="
|
|
254
|
+
<UFormField :label="t('supabase.connection.password')" required>
|
|
249
255
|
<UInput
|
|
250
256
|
v-model="databaseForm.password"
|
|
251
257
|
:disabled="isSettingUpDatabase"
|
|
@@ -261,42 +267,50 @@ function handleStepChange(step: number) {
|
|
|
261
267
|
variant="outline"
|
|
262
268
|
>
|
|
263
269
|
<template #description>
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
<ULink
|
|
267
|
-
to="https://supabase.com/dashboard/project/_/database/settings"
|
|
268
|
-
target="_blank"
|
|
270
|
+
<SupabaseMessageParts
|
|
271
|
+
:message="t('supabase.connection.passwordInfo')"
|
|
269
272
|
>
|
|
270
|
-
|
|
271
|
-
|
|
273
|
+
<template #link>
|
|
274
|
+
<ULink
|
|
275
|
+
to="https://supabase.com/dashboard/project/_/database/settings"
|
|
276
|
+
target="_blank"
|
|
277
|
+
>
|
|
278
|
+
{{ t('supabase.connection.passwordInfoLink') }}
|
|
279
|
+
</ULink>
|
|
280
|
+
</template>
|
|
281
|
+
</SupabaseMessageParts>
|
|
272
282
|
</template>
|
|
273
283
|
</UAlert>
|
|
274
284
|
|
|
275
285
|
<UAlert
|
|
286
|
+
:title="t('supabase.connection.whyTitle')"
|
|
276
287
|
color="warning"
|
|
277
288
|
variant="outline"
|
|
278
|
-
title="Why do I need to provide a database password?"
|
|
279
289
|
icon="lucide:info"
|
|
280
290
|
>
|
|
281
291
|
<template #description>
|
|
282
292
|
<div class="flex flex-col gap-y-4">
|
|
283
293
|
<p>
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
<UTooltip
|
|
287
|
-
text="Click on the 'Technical details' button for more info"
|
|
294
|
+
<SupabaseMessageParts
|
|
295
|
+
:message="t('supabase.setup.why')"
|
|
288
296
|
>
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
297
|
+
<template #promise>
|
|
298
|
+
<UTooltip :text="t('supabase.setup.promiseTooltip')">
|
|
299
|
+
<span
|
|
300
|
+
class="underline decoration-dotted cursor-help"
|
|
301
|
+
>
|
|
302
|
+
{{ t('supabase.setup.promise') }}</span
|
|
303
|
+
></UTooltip
|
|
304
|
+
>
|
|
305
|
+
</template>
|
|
306
|
+
</SupabaseMessageParts>
|
|
293
307
|
</p>
|
|
294
308
|
|
|
295
309
|
<div>
|
|
296
310
|
<UPopover :content="{ side: 'bottom' }" arrow>
|
|
297
311
|
<UButton
|
|
312
|
+
:label="t('supabase.setup.technicalDetails')"
|
|
298
313
|
icon="lucide:lightbulb"
|
|
299
|
-
label="Technical details"
|
|
300
314
|
color="info"
|
|
301
315
|
variant="subtle"
|
|
302
316
|
class="w-fit"
|
|
@@ -308,31 +322,32 @@ function handleStepChange(step: number) {
|
|
|
308
322
|
class="flex flex-col gap-y-4 max-w-xs p-4 text-white text-sm"
|
|
309
323
|
>
|
|
310
324
|
<p>
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
to="https://github.com/plutocms/supabase/blob/feat/setup-wizard/server/api/setup/create.post.ts"
|
|
314
|
-
target="_blank"
|
|
315
|
-
class="underline"
|
|
316
|
-
>
|
|
317
|
-
a server route</ULink
|
|
318
|
-
>
|
|
319
|
-
will be made with the database password you
|
|
320
|
-
provided. This API will then use this password
|
|
321
|
-
to make a direct PostgreSQL connection to your
|
|
322
|
-
Supabase database and run the SQL queries from
|
|
323
|
-
this
|
|
324
|
-
<ULink
|
|
325
|
-
to="https://github.com/plutocms/supabase/tree/main/db/migrations"
|
|
326
|
-
target="_blank"
|
|
327
|
-
class="underline"
|
|
325
|
+
<SupabaseMessageParts
|
|
326
|
+
:message="t('supabase.setup.technicalDetailsRequest')"
|
|
328
327
|
>
|
|
329
|
-
|
|
330
|
-
|
|
328
|
+
<template #route>
|
|
329
|
+
<ULink
|
|
330
|
+
to="https://github.com/plutocms/supabase/blob/feat/setup-wizard/server/api/setup/create.post.ts"
|
|
331
|
+
target="_blank"
|
|
332
|
+
class="underline"
|
|
333
|
+
>
|
|
334
|
+
{{ t('supabase.setup.technicalDetailsRoute') }}</ULink
|
|
335
|
+
>
|
|
336
|
+
</template>
|
|
337
|
+
<template #files>
|
|
338
|
+
<ULink
|
|
339
|
+
to="https://github.com/plutocms/supabase/tree/main/db/migrations"
|
|
340
|
+
target="_blank"
|
|
341
|
+
class="underline"
|
|
342
|
+
>
|
|
343
|
+
{{ t('supabase.setup.technicalDetailsFiles') }}</ULink
|
|
344
|
+
>
|
|
345
|
+
</template>
|
|
346
|
+
</SupabaseMessageParts>
|
|
331
347
|
</p>
|
|
332
348
|
|
|
333
349
|
<p>
|
|
334
|
-
|
|
335
|
-
NOT be stored or used for any other purpose.
|
|
350
|
+
{{ t('supabase.setup.technicalDetailsStored') }}
|
|
336
351
|
</p>
|
|
337
352
|
</div>
|
|
338
353
|
</template>
|
|
@@ -344,11 +359,11 @@ function handleStepChange(step: number) {
|
|
|
344
359
|
|
|
345
360
|
<UAlert
|
|
346
361
|
v-if="isDev"
|
|
362
|
+
:title="t('supabase.connection.restartTitle')"
|
|
363
|
+
:description="t('supabase.setup.restartDescription')"
|
|
347
364
|
color="info"
|
|
348
365
|
variant="outline"
|
|
349
|
-
title="This restarts the dev server"
|
|
350
366
|
icon="lucide:refresh-cw"
|
|
351
|
-
description="Saving your 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."
|
|
352
367
|
/>
|
|
353
368
|
</UForm>
|
|
354
369
|
</section>
|
|
@@ -358,12 +373,11 @@ function handleStepChange(step: number) {
|
|
|
358
373
|
<hgroup class="text-center">
|
|
359
374
|
<span class="text-3xl">🎉</span>
|
|
360
375
|
|
|
361
|
-
<h1 class="font-bold text-2xl">
|
|
376
|
+
<h1 class="font-bold text-2xl">{{ t('supabase.setup.doneTitle') }}</h1>
|
|
362
377
|
</hgroup>
|
|
363
378
|
|
|
364
379
|
<p class="text-balance text-center text-lg">
|
|
365
|
-
|
|
366
|
-
up.
|
|
380
|
+
{{ t('supabase.setup.doneDescription') }}
|
|
367
381
|
</p>
|
|
368
382
|
|
|
369
383
|
<EnvPersistWarning
|
|
@@ -378,7 +392,7 @@ function handleStepChange(step: number) {
|
|
|
378
392
|
color="primary"
|
|
379
393
|
trailing
|
|
380
394
|
>
|
|
381
|
-
|
|
395
|
+
{{ t('supabase.setup.goToSignup') }}
|
|
382
396
|
</UButton>
|
|
383
397
|
</p>
|
|
384
398
|
</div>
|
package/app/pages/admin.vue
CHANGED
|
@@ -5,6 +5,7 @@ definePageMeta({
|
|
|
5
5
|
})
|
|
6
6
|
|
|
7
7
|
const route = useRoute()
|
|
8
|
+
const { t } = usePlutoI18n()
|
|
8
9
|
const { isLoggedIn, logout, allowedUnauthenticatedPaths } = await useAuth()
|
|
9
10
|
const toast = useToast()
|
|
10
11
|
|
|
@@ -26,8 +27,8 @@ watch(visibility, async (current, previous) => {
|
|
|
26
27
|
await logout({ redirectTo: route.path })
|
|
27
28
|
|
|
28
29
|
toast.add({
|
|
29
|
-
title: '
|
|
30
|
-
description: '
|
|
30
|
+
title: t('supabase.auth.toasts.notLoggedIn'),
|
|
31
|
+
description: t('supabase.auth.toasts.notLoggedInDescription'),
|
|
31
32
|
icon: 'lucide:circle-x',
|
|
32
33
|
color: 'error',
|
|
33
34
|
})
|
|
@@ -2,6 +2,10 @@ import NavbarAdminProvider from '../components/navbar/NavbarAdminProvider.vue'
|
|
|
2
2
|
import SettingsGeneralPanel from '../components/settings/SettingsGeneralPanel.vue'
|
|
3
3
|
|
|
4
4
|
export default defineNuxtPlugin(() => {
|
|
5
|
+
// Read at setup, while the Nuxt context exists. The driver below calls it
|
|
6
|
+
// later, after an await, when the context is gone.
|
|
7
|
+
const errorText = usePlutoErrorText()
|
|
8
|
+
|
|
5
9
|
definePlutoExtension({
|
|
6
10
|
id: 'supabase',
|
|
7
11
|
navbar: { id: 'admin-navbar', component: NavbarAdminProvider },
|
|
@@ -9,7 +13,7 @@ export default defineNuxtPlugin(() => {
|
|
|
9
13
|
{
|
|
10
14
|
id: 'general',
|
|
11
15
|
order: 0,
|
|
12
|
-
title: '
|
|
16
|
+
title: 'supabase.settings.general.title',
|
|
13
17
|
icon: 'lucide:globe',
|
|
14
18
|
component: SettingsGeneralPanel,
|
|
15
19
|
},
|
|
@@ -40,9 +44,26 @@ export default defineNuxtPlugin(() => {
|
|
|
40
44
|
// gated by requireAdmin (public.is_admin() directly), never a named
|
|
41
45
|
// capability — see server/utils/admin-guard.ts for why.
|
|
42
46
|
capabilities: [
|
|
43
|
-
{ id: 'settings-manage', key: 'settings:manage', label: '
|
|
44
|
-
{ id: 'users-read', key: 'users:read', label: '
|
|
47
|
+
{ id: 'settings-manage', key: 'settings:manage', label: 'supabase.capabilities.settingsManage' },
|
|
48
|
+
{ id: 'users-read', key: 'users:read', label: 'supabase.capabilities.usersRead' },
|
|
45
49
|
],
|
|
50
|
+
// The admin language of the signed-in user, stored in profiles.locale
|
|
51
|
+
// (005_profile_locale.sql). A save error carries the translated server
|
|
52
|
+
// text, so the Settings language card can show it in a toast.
|
|
53
|
+
localeDriver: {
|
|
54
|
+
id: 'supabase',
|
|
55
|
+
load: async () => {
|
|
56
|
+
const response = await $fetch<{ locale: string | null }>('/api/profile/locale')
|
|
57
|
+
return response.locale
|
|
58
|
+
},
|
|
59
|
+
save: async (locale) => {
|
|
60
|
+
try {
|
|
61
|
+
await $fetch('/api/profile/locale', { method: 'POST', body: { locale } })
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(errorText(error))
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
},
|
|
46
67
|
permissionsDriver: {
|
|
47
68
|
id: 'supabase',
|
|
48
69
|
load: async () => {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads the signed-in user's stored admin language whenever a session
|
|
3
|
+
* appears: after a login, and on a page load that already has a session.
|
|
4
|
+
* App-wide, like pluto-permissions-sync.ts, so any page gets the language,
|
|
5
|
+
* not only the pages under /admin.
|
|
6
|
+
*
|
|
7
|
+
* Client-only, unlike the permissions sync. A server `$fetch` does not forward
|
|
8
|
+
* the user's cookies, so a server run would read no locale and clear the cookie.
|
|
9
|
+
* The server already resolves the first locale from the cookie or the
|
|
10
|
+
* Accept-Language header. The stored value then replaces it on the client, and
|
|
11
|
+
* the cookie keeps it for the next server render.
|
|
12
|
+
*
|
|
13
|
+
* `useSupabaseSession()` is the same primitive the permissions sync uses.
|
|
14
|
+
* `syncUserLocale()` never throws. With no driver, or on an error, nothing
|
|
15
|
+
* changes.
|
|
16
|
+
*/
|
|
17
|
+
export default defineNuxtPlugin(() => {
|
|
18
|
+
const session = useSupabaseSession()
|
|
19
|
+
const { syncUserLocale } = usePlutoI18n()
|
|
20
|
+
|
|
21
|
+
watch(
|
|
22
|
+
session,
|
|
23
|
+
(current) => {
|
|
24
|
+
if (current) {
|
|
25
|
+
void syncUserLocale()
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{ immediate: true }
|
|
29
|
+
)
|
|
30
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Turns a Supabase auth error into text for the admin. This file does not use
|
|
2
|
+
// Nuxt auto-imports, so plain Vitest can import it.
|
|
3
|
+
|
|
4
|
+
/** The translation key of each Supabase auth error code that the admin knows. */
|
|
5
|
+
export const AUTH_ERROR_KEYS: Record<string, string> = {
|
|
6
|
+
invalid_credentials: 'supabase.auth.errors.invalid_credentials',
|
|
7
|
+
email_not_confirmed: 'supabase.auth.errors.email_not_confirmed',
|
|
8
|
+
over_request_rate_limit: 'supabase.auth.errors.over_request_rate_limit',
|
|
9
|
+
over_email_send_rate_limit: 'supabase.auth.errors.over_email_send_rate_limit',
|
|
10
|
+
weak_password: 'supabase.auth.errors.weak_password',
|
|
11
|
+
same_password: 'supabase.auth.errors.same_password',
|
|
12
|
+
user_not_found: 'supabase.auth.errors.user_not_found',
|
|
13
|
+
email_exists: 'supabase.auth.errors.email_exists',
|
|
14
|
+
user_already_exists: 'supabase.auth.errors.user_already_exists',
|
|
15
|
+
signup_disabled: 'supabase.auth.errors.signup_disabled',
|
|
16
|
+
email_address_invalid: 'supabase.auth.errors.email_address_invalid',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The key for errors with no code, no message, or an unknown shape. */
|
|
20
|
+
export const AUTH_UNKNOWN_ERROR_KEY = 'supabase.auth.errors.unknown'
|
|
21
|
+
|
|
22
|
+
/** Returns the translation key of an auth error code, or `undefined`. */
|
|
23
|
+
export function authErrorKey(error: unknown): string | undefined {
|
|
24
|
+
const code = (error as { code?: unknown } | null | undefined)?.code
|
|
25
|
+
|
|
26
|
+
return typeof code === 'string' && Object.hasOwn(AUTH_ERROR_KEYS, code)
|
|
27
|
+
? AUTH_ERROR_KEYS[code]
|
|
28
|
+
: undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns the text of an auth error. The order is:
|
|
33
|
+
*
|
|
34
|
+
* 1. The translated text of a known `error.code`.
|
|
35
|
+
* 2. The raw `error.message`.
|
|
36
|
+
* 3. The translated unknown-error text.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveAuthErrorText(
|
|
39
|
+
error: unknown,
|
|
40
|
+
i18n: { t: (key: string) => string, te: (key: string) => boolean }
|
|
41
|
+
): string {
|
|
42
|
+
const key = authErrorKey(error)
|
|
43
|
+
|
|
44
|
+
if (key && i18n.te(key)) {
|
|
45
|
+
return i18n.t(key)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const message = (error as { message?: unknown } | null | undefined)?.message
|
|
49
|
+
|
|
50
|
+
return typeof message === 'string' && message.trim() !== ''
|
|
51
|
+
? message
|
|
52
|
+
: i18n.t(AUTH_UNKNOWN_ERROR_KEY)
|
|
53
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
-- Profile language
|
|
2
|
+
--
|
|
3
|
+
-- Stores each user's admin language on their own profile row. NULL means
|
|
4
|
+
-- "Automatic": the user has not chosen one, so the admin uses the browser
|
|
5
|
+
-- language. The format check accepts any BCP 47 style code ('en', 'pt-BR'),
|
|
6
|
+
-- never a fixed list, so a new language needs no migration. The app decides
|
|
7
|
+
-- which codes it offers.
|
|
8
|
+
--
|
|
9
|
+
-- There is no UPDATE policy on public.profiles, on purpose: a user must never
|
|
10
|
+
-- write their own is_admin column. A SECURITY DEFINER function is the only
|
|
11
|
+
-- writer of this column. It touches the row of auth.uid() and no other row,
|
|
12
|
+
-- and it touches only the locale. This migration adds no RLS policy. The
|
|
13
|
+
-- existing select policy already lets a user read their own row.
|
|
14
|
+
|
|
15
|
+
alter table public.profiles add column if not exists locale text;
|
|
16
|
+
|
|
17
|
+
do $$
|
|
18
|
+
begin
|
|
19
|
+
if not exists (
|
|
20
|
+
select 1 from pg_constraint
|
|
21
|
+
where conname = 'profiles_locale_format'
|
|
22
|
+
and conrelid = 'public.profiles'::regclass
|
|
23
|
+
) then
|
|
24
|
+
alter table public.profiles
|
|
25
|
+
add constraint profiles_locale_format
|
|
26
|
+
check (locale is null or locale ~ '^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$') not valid;
|
|
27
|
+
end if;
|
|
28
|
+
end
|
|
29
|
+
$$;
|
|
30
|
+
|
|
31
|
+
alter table public.profiles validate constraint profiles_locale_format;
|
|
32
|
+
|
|
33
|
+
-- Sets the language of the calling user. Pass NULL to choose Automatic.
|
|
34
|
+
create or replace function public.set_my_locale(new_locale text)
|
|
35
|
+
returns void
|
|
36
|
+
language sql security definer set search_path = ''
|
|
37
|
+
as $$
|
|
38
|
+
update public.profiles
|
|
39
|
+
set locale = new_locale, updated_at = now()
|
|
40
|
+
where id = auth.uid();
|
|
41
|
+
$$;
|
|
42
|
+
|
|
43
|
+
revoke execute on function public.set_my_locale(text) from public, anon;
|
|
44
|
+
grant execute on function public.set_my_locale(text) to authenticated;
|