@plutocms/supabase 0.9.1 → 0.11.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 +19 -0
- package/FEATURES.md +9 -0
- package/README.md +10 -1
- package/app/components/EnvPersistWarning.vue +30 -13
- package/app/components/SupabaseMessageParts.vue +29 -0
- package/app/components/auth/AuthSignIn.vue +56 -0
- package/app/components/migrations/MigrationsBanner.vue +28 -15
- package/app/components/settings/SettingsGeneralPanel.vue +21 -6
- package/app/composables/auth.ts +38 -101
- package/app/pages/admin/(auth)/confirm.vue +20 -23
- package/app/pages/admin/(auth)/forgot-password.vue +60 -63
- package/app/pages/admin/(auth)/signup.vue +115 -110
- package/app/pages/admin/(auth)/update-password.vue +78 -82
- package/app/pages/admin/migrations.vue +87 -51
- package/app/pages/admin/setup.vue +88 -75
- package/app/plugins/pluto-extension.ts +143 -10
- package/app/utils/auth-driver.ts +110 -0
- package/app/utils/auth-error.ts +53 -0
- package/db/migrations/005_profile_locale.sql +44 -0
- package/i18n/locales/en.json +193 -0
- package/i18n/locales/pt-BR.json +193 -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
- package/app/components/navbar/NavbarAdmin.vue +0 -213
- package/app/components/navbar/NavbarAdminProvider.vue +0 -18
- package/app/middleware/auth.ts +0 -34
- package/app/middleware/setup-check.ts +0 -44
- package/app/pages/admin/(auth)/login.vue +0 -93
- package/app/pages/admin.vue +0 -56
- package/app/plugins/pluto-permissions-sync.ts +0 -38
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
{
|
|
2
|
+
"supabase": {
|
|
3
|
+
"profile": {
|
|
4
|
+
"migrationPending": "Aplique as migrações pendentes do banco de dados para salvar seu idioma."
|
|
5
|
+
},
|
|
6
|
+
"errors": {
|
|
7
|
+
"notLoggedIn": "Entre na sua conta para continuar.",
|
|
8
|
+
"notAdmin": "Sua conta não é de administrador.",
|
|
9
|
+
"missingPermission": "Sua conta não tem a permissão \"{capability}\".",
|
|
10
|
+
"setupCompleted": "A configuração já foi concluída.",
|
|
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
|
+
"localeUnsupported": "Este idioma não está disponível.",
|
|
15
|
+
"duplicateRecord": "Já existe um registro com este valor.",
|
|
16
|
+
"missingReference": "Isto aponta para um registro que não existe.",
|
|
17
|
+
"migrationStatus": "Erro ao ler o status das migrações: {message}"
|
|
18
|
+
},
|
|
19
|
+
"auth": {
|
|
20
|
+
"backToLogin": "Voltar para o login",
|
|
21
|
+
"passwordMismatch": "As senhas não coincidem.",
|
|
22
|
+
"fields": {
|
|
23
|
+
"email": "E-mail",
|
|
24
|
+
"password": "Senha",
|
|
25
|
+
"username": "Nome de usuário",
|
|
26
|
+
"displayName": "Nome de exibição",
|
|
27
|
+
"confirmPassword": "Confirmar senha"
|
|
28
|
+
},
|
|
29
|
+
"toasts": {
|
|
30
|
+
"loginFailed": "Não foi possível entrar",
|
|
31
|
+
"resetFailed": "Não foi possível enviar o e-mail de redefinição",
|
|
32
|
+
"updateFailed": "Não foi possível atualizar a senha",
|
|
33
|
+
"passwordUpdated": "Senha atualizada com sucesso"
|
|
34
|
+
},
|
|
35
|
+
"errors": {
|
|
36
|
+
"unknown": "Ocorreu um erro desconhecido.",
|
|
37
|
+
"invalid_credentials": "E-mail ou senha inválidos.",
|
|
38
|
+
"email_not_confirmed": "Seu e-mail ainda não foi confirmado. Verifique sua caixa de entrada para encontrar o link de verificação.",
|
|
39
|
+
"over_request_rate_limit": "Muitas tentativas. Aguarde um momento e tente novamente.",
|
|
40
|
+
"over_email_send_rate_limit": "Muitos e-mails enviados. Aguarde um momento e tente novamente.",
|
|
41
|
+
"weak_password": "Esta senha é muito fraca. Escolha uma senha mais forte.",
|
|
42
|
+
"same_password": "A nova senha deve ser diferente da senha atual.",
|
|
43
|
+
"user_not_found": "Nenhuma conta foi encontrada para este e-mail.",
|
|
44
|
+
"email_exists": "Já existe uma conta com este e-mail.",
|
|
45
|
+
"user_already_exists": "Já existe uma conta com este e-mail.",
|
|
46
|
+
"signup_disabled": "O cadastro está desativado neste site.",
|
|
47
|
+
"email_address_invalid": "Este endereço de e-mail não é válido."
|
|
48
|
+
},
|
|
49
|
+
"confirm": {
|
|
50
|
+
"pageTitle": "Verificação da conta",
|
|
51
|
+
"title": "Conta verificada!",
|
|
52
|
+
"description": "Sua conta foi verificada com sucesso. Agora você já pode usar o painel.",
|
|
53
|
+
"goToDashboard": "Ir para o painel"
|
|
54
|
+
},
|
|
55
|
+
"forgot": {
|
|
56
|
+
"pageTitle": "Esqueceu a senha?",
|
|
57
|
+
"title": "Esqueceu a senha?",
|
|
58
|
+
"description": "Informe seu e-mail e enviaremos um link de redefinição.",
|
|
59
|
+
"submit": "Enviar link de redefinição",
|
|
60
|
+
"sentTitle": "Verifique sua caixa de entrada",
|
|
61
|
+
"sentDescription": "Se existir uma conta para {email}, enviamos um link de redefinição de senha. Verifique seu e-mail e siga o link."
|
|
62
|
+
},
|
|
63
|
+
"login": {
|
|
64
|
+
"title": "Entrar",
|
|
65
|
+
"submit": "Entrar"
|
|
66
|
+
},
|
|
67
|
+
"update": {
|
|
68
|
+
"title": "Atualizar senha",
|
|
69
|
+
"description": "Informe sua nova senha abaixo.",
|
|
70
|
+
"newPassword": "Nova senha",
|
|
71
|
+
"confirmNewPassword": "Confirmar nova senha",
|
|
72
|
+
"submit": "Atualizar senha",
|
|
73
|
+
"doneTitle": "Senha atualizada!",
|
|
74
|
+
"doneDescription": "Sua senha foi alterada. Agora você já pode entrar com a nova senha.",
|
|
75
|
+
"goToLogin": "Ir para o login"
|
|
76
|
+
},
|
|
77
|
+
"signup": {
|
|
78
|
+
"pageTitle": "Cadastro",
|
|
79
|
+
"title": "Criar conta",
|
|
80
|
+
"submit": "Criar conta",
|
|
81
|
+
"usernamePlaceholder": "ex.: joaosilva",
|
|
82
|
+
"displayNamePlaceholder": "ex.: João Silva",
|
|
83
|
+
"created": "Conta criada com sucesso",
|
|
84
|
+
"createdDescription": "Ainda é preciso verificar seu e-mail para poder entrar.",
|
|
85
|
+
"failed": "Não foi possível criar a conta",
|
|
86
|
+
"almostThere": "Quase lá!",
|
|
87
|
+
"verificationSent": "Enviamos um link de verificação para o seu e-mail. Verifique sua caixa de entrada e siga as instruções para verificar sua conta.",
|
|
88
|
+
"closePage": "Você pode fechar esta página. Você entrará automaticamente assim que verificar seu e-mail.",
|
|
89
|
+
"checkSpam": "Se você não encontrar o e-mail, verifique sua pasta de spam."
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"settings": {
|
|
93
|
+
"general": {
|
|
94
|
+
"title": "Geral",
|
|
95
|
+
"websiteTitle": "Título do site",
|
|
96
|
+
"websiteTitlePlaceholder": "ex.: Meu site",
|
|
97
|
+
"websiteDescription": "Descrição do site",
|
|
98
|
+
"websiteDescriptionPlaceholder": "ex.: Descrição do meu site",
|
|
99
|
+
"websiteUrl": "URL do site",
|
|
100
|
+
"websiteUrlPlaceholder": "ex.: https://exemplo.com"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"capabilities": {
|
|
104
|
+
"settingsManage": "Gerenciar as configurações do site",
|
|
105
|
+
"usersRead": "Ver todas as contas de usuário"
|
|
106
|
+
},
|
|
107
|
+
"envWarning": {
|
|
108
|
+
"title": "A string de conexão não foi salva automaticamente",
|
|
109
|
+
"intro": "A alteração no banco de dados já foi concluída. O Pluto não conseguiu gravar {variable} no arquivo {file} do seu projeto. Por isso, ele vai pedir de novo na próxima vez, a menos que você adicione a linha manualmente:",
|
|
110
|
+
"step1": "Abra (ou crie) o arquivo {file} na pasta raiz do seu projeto.",
|
|
111
|
+
"step2": "Adicione esta linha. Substitua qualquer linha {variable} existente por ela.",
|
|
112
|
+
"step3": "Reinicie o servidor.",
|
|
113
|
+
"copyLine": "Copiar a linha DATABASE_URL",
|
|
114
|
+
"copied": "Copiado",
|
|
115
|
+
"copiedDescription": "A linha DATABASE_URL está na sua área de transferência.",
|
|
116
|
+
"copyFailed": "Não foi possível copiar",
|
|
117
|
+
"copyFailedDescription": "Selecione e copie a linha manualmente."
|
|
118
|
+
},
|
|
119
|
+
"connection": {
|
|
120
|
+
"connectionString": "String de conexão do Supabase",
|
|
121
|
+
"connectionStringHelp": "Você encontra sua string de conexão {link}.",
|
|
122
|
+
"connectionStringHelpLink": "clicando aqui",
|
|
123
|
+
"password": "Senha do banco de dados",
|
|
124
|
+
"passwordInfo": "Sua senha é criada quando você cria seu projeto no Supabase. Você pode alterá-la em {link}.",
|
|
125
|
+
"passwordInfoLink": "Configurações do banco de dados do Supabase",
|
|
126
|
+
"whyTitle": "Por que preciso informar a senha do banco de dados?",
|
|
127
|
+
"restartTitle": "Isto reinicia o servidor de desenvolvimento",
|
|
128
|
+
"restarting": "Servidor de desenvolvimento reiniciando",
|
|
129
|
+
"reconnected": "Reconectado",
|
|
130
|
+
"stillReconnecting": "Ainda reconectando",
|
|
131
|
+
"stillReconnectingDescription": "O servidor de desenvolvimento está demorando mais que o esperado. Recarregue esta página em instantes.",
|
|
132
|
+
"checkConsole": "Verifique o console para ver mais detalhes."
|
|
133
|
+
},
|
|
134
|
+
"setup": {
|
|
135
|
+
"title": "Assistente de configuração",
|
|
136
|
+
"steps": {
|
|
137
|
+
"database": "Configuração do banco de dados",
|
|
138
|
+
"complete": "Concluir configuração"
|
|
139
|
+
},
|
|
140
|
+
"submit": "Configurar banco de dados",
|
|
141
|
+
"next": "Enviar",
|
|
142
|
+
"why": "Ela será usada para criar a estrutura do seu banco de dados. Ela não será armazenada em nenhum lugar. Isso é uma {promise}! 😉",
|
|
143
|
+
"promise": "promessa",
|
|
144
|
+
"promiseTooltip": "Clique no botão 'Detalhes técnicos' para saber mais",
|
|
145
|
+
"technicalDetails": "Detalhes técnicos",
|
|
146
|
+
"technicalDetailsRequest": "Ao clicar em \"Próximo\", será feita uma requisição para {route} com a senha do banco de dados que você informou. Essa API usará a senha para abrir uma conexão direta do PostgreSQL com seu banco de dados do Supabase e executar as consultas SQL destes {files}.",
|
|
147
|
+
"technicalDetailsRoute": "uma rota do servidor",
|
|
148
|
+
"technicalDetailsFiles": "arquivos SQL",
|
|
149
|
+
"technicalDetailsStored": "Depois que a configuração terminar, a senha NÃO será armazenada nem usada para nenhum outro fim.",
|
|
150
|
+
"restartDescription": "Salvar sua string de conexão a grava no arquivo .env. Em desenvolvimento, isso reinicia o servidor automaticamente. A página vai exibir brevemente uma mensagem de reconexão. Isso é esperado e não é um erro.",
|
|
151
|
+
"restartingDescription": "A configuração está terminando. Reconectando…",
|
|
152
|
+
"reconnectedDescription": "O servidor voltou. Seu banco de dados foi configurado.",
|
|
153
|
+
"failed": "Falha ao configurar o banco de dados",
|
|
154
|
+
"failedDescription": "Verifique sua senha e tente novamente.",
|
|
155
|
+
"complete": "Banco de dados configurado",
|
|
156
|
+
"completeDescription": "Seu banco de dados foi configurado com sucesso.",
|
|
157
|
+
"error": "Erro ao configurar o banco de dados",
|
|
158
|
+
"doneTitle": "Configuração concluída",
|
|
159
|
+
"doneDescription": "Seu banco de dados está pronto. Crie sua conta de administrador fazendo o cadastro.",
|
|
160
|
+
"goToSignup": "Ir para o cadastro"
|
|
161
|
+
},
|
|
162
|
+
"migrations": {
|
|
163
|
+
"title": "Migrações",
|
|
164
|
+
"banner": "{count} camada precisa de uma migração do banco de dados | {count} camadas precisam de uma migração do banco de dados",
|
|
165
|
+
"goTo": "Ir para as migrações",
|
|
166
|
+
"loading": "Carregando o status das migrações…",
|
|
167
|
+
"upToDate": "Tudo está atualizado",
|
|
168
|
+
"upToDateDescription": "Nenhuma camada precisa de migração do banco de dados.",
|
|
169
|
+
"pendingLayers": "Camadas pendentes ({count})",
|
|
170
|
+
"appliedLayers": "Camadas aplicadas ({count})",
|
|
171
|
+
"why": "Ela é usada uma única vez para abrir uma conexão direta do PostgreSQL e executar os esquemas pendentes das camadas. Depois, é gravada no arquivo .env do servidor, e as próximas migrações não precisam pedir a senha. Ela nunca é armazenada em outro lugar e nunca é enviada de volta ao seu navegador.",
|
|
172
|
+
"restartDescription": "Salvar uma nova string de conexão a grava no arquivo .env. Em desenvolvimento, isso reinicia o servidor automaticamente. A página vai exibir brevemente uma mensagem de reconexão. Isso é esperado e não é um erro.",
|
|
173
|
+
"restartingDescription": "Salvar uma nova string de conexão reinicia o servidor de desenvolvimento. Reconectando…",
|
|
174
|
+
"reconnectingBanner": "Salvar uma nova string de conexão reinicia o servidor de desenvolvimento. Reconectando — esta página será atualizada sozinha quando o servidor voltar.",
|
|
175
|
+
"reconnectedAllApplied": "O servidor voltou. Todas as migrações foram aplicadas.",
|
|
176
|
+
"reconnectedPending": "O servidor voltou. {count} camada(s) ainda pendente(s) — verifique acima.",
|
|
177
|
+
"apply": "Aplicar migrações pendentes",
|
|
178
|
+
"applied": "Migrações aplicadas",
|
|
179
|
+
"applyFailed": "Não foi possível aplicar as migrações",
|
|
180
|
+
"connectionRequired": "É necessária uma string de conexão do banco de dados.",
|
|
181
|
+
"error": "Erro ao aplicar as migrações",
|
|
182
|
+
"lastRun": "Última execução",
|
|
183
|
+
"status": {
|
|
184
|
+
"applied": "aplicada",
|
|
185
|
+
"skipped": "ignorada",
|
|
186
|
+
"failed": "falhou"
|
|
187
|
+
},
|
|
188
|
+
"loadFailed": "Não foi possível carregar o status das migrações",
|
|
189
|
+
"loadFailedDescription": "Este não é o caso de reinício do servidor de desenvolvimento descrito acima. Algo diferente deu errado ao ler o status das migrações. Verifique o console do servidor e tente novamente.",
|
|
190
|
+
"retry": "Tentar novamente"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plutocms/supabase",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.0",
|
|
5
5
|
"trustedDependencies": [
|
|
6
6
|
"@parcel/watcher",
|
|
7
7
|
"@plutocms/pluto",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"postinstall": "nuxt prepare",
|
|
31
31
|
"prettier": "prettier --write .",
|
|
32
32
|
"lint": "eslint .",
|
|
33
|
-
"lint:fix": "eslint . --fix
|
|
33
|
+
"lint:fix": "eslint . --fix",
|
|
34
34
|
"typecheck": "nuxi typecheck",
|
|
35
35
|
"test": "vitest run",
|
|
36
36
|
"supabase-types": "bun ./scripts/supabase-typegen.ts"
|
|
@@ -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.
|
|
42
|
+
"@plutocms/pluto": "^0.16.0",
|
|
43
43
|
"@plutocms/utils": "^0.2.1",
|
|
44
44
|
"postgres": "^3.4.9",
|
|
45
45
|
"supabase": "^2.121.0",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { serverSupabaseClient, serverSupabaseUser } from '#supabase/server'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The calling user's own admin language. `null` means Automatic: the user
|
|
5
|
+
* has not chosen one. No admin gate: every signed-in user reads their own
|
|
6
|
+
* row. A signed-out caller gets `null`, not a 401, so the client never has
|
|
7
|
+
* to special-case this route's error path.
|
|
8
|
+
*
|
|
9
|
+
* Read `user.sub`, not `user.id`. See server/utils/admin-guard.ts.
|
|
10
|
+
*/
|
|
11
|
+
export default defineEventHandler(async (event) => {
|
|
12
|
+
const user = await serverSupabaseUser(event)
|
|
13
|
+
|
|
14
|
+
if (!user) {
|
|
15
|
+
return { locale: null as string | null }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const client = await serverSupabaseClient<Database>(event)
|
|
19
|
+
const { data, error } = await client
|
|
20
|
+
.from('profiles')
|
|
21
|
+
.select('locale')
|
|
22
|
+
.eq('id', user.sub)
|
|
23
|
+
.maybeSingle()
|
|
24
|
+
|
|
25
|
+
if (error) {
|
|
26
|
+
// profiles.locale is added by 005_profile_locale.sql. A site that has
|
|
27
|
+
// upgraded this package but not yet applied that migration has no such
|
|
28
|
+
// column. That state is expected and temporary, so degrade to
|
|
29
|
+
// "Automatic" instead of a 500. The same choice is made in
|
|
30
|
+
// server/api/permissions/me.get.ts.
|
|
31
|
+
console.error(
|
|
32
|
+
'profiles.locale read failed, returning no locale:',
|
|
33
|
+
error.message
|
|
34
|
+
)
|
|
35
|
+
return { locale: null as string | null }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { locale: data?.locale ?? null }
|
|
39
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { serverSupabaseClient, serverSupabaseUser } from '#supabase/server'
|
|
3
|
+
import { parseBody } from '../../utils/validate'
|
|
4
|
+
|
|
5
|
+
const localeSchema = z.object({
|
|
6
|
+
locale: z.string().nullable(),
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* True when PostgREST says the function does not exist. PGRST202 is
|
|
11
|
+
* PostgREST's own "function not found" code. 42883 is the Postgres code.
|
|
12
|
+
*/
|
|
13
|
+
function isMissingFunction(error: {
|
|
14
|
+
code?: string
|
|
15
|
+
message?: string
|
|
16
|
+
}): boolean {
|
|
17
|
+
return (
|
|
18
|
+
error.code === 'PGRST202' ||
|
|
19
|
+
error.code === '42883' ||
|
|
20
|
+
/could not find the function/i.test(error.message ?? '')
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Stores the calling user's admin language. Pass `null` for Automatic.
|
|
26
|
+
*
|
|
27
|
+
* The write goes through `public.set_my_locale()`, a SECURITY DEFINER
|
|
28
|
+
* function from 005_profile_locale.sql. `profiles` has no UPDATE policy on
|
|
29
|
+
* purpose, so a user can never write `is_admin`.
|
|
30
|
+
*/
|
|
31
|
+
export default defineEventHandler(async (event) => {
|
|
32
|
+
const user = await serverSupabaseUser(event)
|
|
33
|
+
|
|
34
|
+
if (!user) {
|
|
35
|
+
throw createPlutoError({
|
|
36
|
+
statusCode: 401,
|
|
37
|
+
message: 'You must be logged in.',
|
|
38
|
+
key: 'supabase.errors.notLoggedIn',
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { locale } = await parseBody(event, localeSchema)
|
|
43
|
+
const { adminLocales } = useRuntimeConfig().public.plutoI18n
|
|
44
|
+
|
|
45
|
+
if (locale !== null && !adminLocales.includes(locale)) {
|
|
46
|
+
throw createPlutoError({
|
|
47
|
+
statusCode: 400,
|
|
48
|
+
message: 'This language is not supported.',
|
|
49
|
+
key: 'supabase.errors.localeUnsupported',
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const client = await serverSupabaseClient<Database>(event)
|
|
54
|
+
const { error } = await client.rpc('set_my_locale', {
|
|
55
|
+
new_locale: locale as string,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (error) {
|
|
59
|
+
if (isMissingFunction(error)) {
|
|
60
|
+
throw createPlutoError({
|
|
61
|
+
statusCode: 409,
|
|
62
|
+
message: 'Apply the pending database migrations to save your language.',
|
|
63
|
+
key: 'supabase.profile.migrationPending',
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw createError({ statusCode: 500, statusMessage: error.message })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { locale }
|
|
71
|
+
})
|
|
@@ -23,9 +23,10 @@ export default defineEventHandler(async (event) => {
|
|
|
23
23
|
// server/api/migrations/run.post.ts, which calls it "the whole security
|
|
24
24
|
// model for this endpoint."
|
|
25
25
|
if (process.env.DATABASE_URL) {
|
|
26
|
-
throw
|
|
26
|
+
throw createPlutoError({
|
|
27
27
|
statusCode: 403,
|
|
28
|
-
|
|
28
|
+
message: 'Setup has already been completed.',
|
|
29
|
+
key: 'supabase.errors.setupCompleted',
|
|
29
30
|
})
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -47,10 +48,11 @@ export default defineEventHandler(async (event) => {
|
|
|
47
48
|
// it over HTTP means this route needs no `baseUrl` from the caller, and
|
|
48
49
|
// can no longer be made to fetch or execute SQL from an arbitrary origin.
|
|
49
50
|
if (!layers.core?.length) {
|
|
50
|
-
throw
|
|
51
|
+
throw createPlutoError({
|
|
51
52
|
statusCode: 500,
|
|
52
|
-
|
|
53
|
+
message:
|
|
53
54
|
'Core schema not found. Reinstall @plutocms/supabase and restart the dev server.',
|
|
55
|
+
key: 'supabase.errors.coreSchemaMissing',
|
|
54
56
|
})
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -14,9 +14,11 @@ export default defineEventHandler(async (event) => {
|
|
|
14
14
|
.eq('id', id)
|
|
15
15
|
|
|
16
16
|
if (error) {
|
|
17
|
-
throw
|
|
17
|
+
throw createPlutoError({
|
|
18
18
|
statusCode: 500,
|
|
19
|
-
|
|
19
|
+
message: `Error fetching user: ${error.message}`,
|
|
20
|
+
key: 'supabase.errors.fetchUser',
|
|
21
|
+
params: { message: error.message },
|
|
20
22
|
})
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -9,9 +9,11 @@ export default defineEventHandler(async (event) => {
|
|
|
9
9
|
const { data: users, error } = await client.from('profiles').select('*')
|
|
10
10
|
|
|
11
11
|
if (error) {
|
|
12
|
-
throw
|
|
12
|
+
throw createPlutoError({
|
|
13
13
|
statusCode: 500,
|
|
14
|
-
|
|
14
|
+
message: `Error fetching users: ${error.message}`,
|
|
15
|
+
key: 'supabase.errors.fetchUsers',
|
|
16
|
+
params: { message: error.message },
|
|
15
17
|
})
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -45,7 +45,11 @@ export async function requireAdmin(event: H3Event) {
|
|
|
45
45
|
const user = await serverSupabaseUser(event)
|
|
46
46
|
|
|
47
47
|
if (!user) {
|
|
48
|
-
throw
|
|
48
|
+
throw createPlutoError({
|
|
49
|
+
statusCode: 401,
|
|
50
|
+
message: 'You must be logged in.',
|
|
51
|
+
key: 'supabase.errors.notLoggedIn',
|
|
52
|
+
})
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
const client = await serverSupabaseClient<Database>(event)
|
|
@@ -64,7 +68,11 @@ export async function requireAdmin(event: H3Event) {
|
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
if (error || !profile?.is_admin) {
|
|
67
|
-
throw
|
|
71
|
+
throw createPlutoError({
|
|
72
|
+
statusCode: 403,
|
|
73
|
+
message: 'Your account is not an admin.',
|
|
74
|
+
key: 'supabase.errors.notAdmin',
|
|
75
|
+
})
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
return user
|
|
@@ -5,8 +5,10 @@ import { serverSupabaseClient, serverSupabaseUser } from '#supabase/server'
|
|
|
5
5
|
export const ALL_CAPABILITIES = '*'
|
|
6
6
|
|
|
7
7
|
interface RequireCapabilityOptions {
|
|
8
|
-
/** Overrides the 403 message. */
|
|
8
|
+
/** Overrides the 403 message. English text. */
|
|
9
9
|
message?: string
|
|
10
|
+
/** Translation key of the `message` override, for the admin UI. */
|
|
11
|
+
messageKey?: string
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -34,17 +36,29 @@ export async function requireCapability(
|
|
|
34
36
|
const user = await serverSupabaseUser(event)
|
|
35
37
|
|
|
36
38
|
if (!user) {
|
|
37
|
-
throw
|
|
39
|
+
throw createPlutoError({
|
|
40
|
+
statusCode: 401,
|
|
41
|
+
message: 'You must be logged in.',
|
|
42
|
+
key: 'supabase.errors.notLoggedIn',
|
|
43
|
+
})
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
const client = await serverSupabaseClient<Database>(event)
|
|
41
47
|
const { data, error } = await client.rpc('has_capability', { cap: capability })
|
|
42
48
|
|
|
43
49
|
if (error || data !== true) {
|
|
44
|
-
|
|
50
|
+
// A custom `message` without a `messageKey` has no translation, so it
|
|
51
|
+
// keeps the plain English error.
|
|
52
|
+
if (options?.message && !options.messageKey) {
|
|
53
|
+
throw createError({ statusCode: 403, statusMessage: options.message })
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw createPlutoError({
|
|
45
57
|
statusCode: 403,
|
|
46
|
-
|
|
58
|
+
message:
|
|
47
59
|
options?.message ?? `Your account is missing the "${capability}" permission.`,
|
|
60
|
+
key: options?.messageKey ?? 'supabase.errors.missingPermission',
|
|
61
|
+
params: { capability },
|
|
48
62
|
})
|
|
49
63
|
}
|
|
50
64
|
|
|
@@ -82,17 +82,19 @@ export function toContentError(error: PostgrestError) {
|
|
|
82
82
|
|
|
83
83
|
// 23505: unique_violation.
|
|
84
84
|
if (error.code === '23505') {
|
|
85
|
-
return
|
|
85
|
+
return createPlutoError({
|
|
86
86
|
statusCode: 409,
|
|
87
|
-
|
|
87
|
+
message: 'A record with this value already exists.',
|
|
88
|
+
key: 'supabase.errors.duplicateRecord',
|
|
88
89
|
})
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
// 23503: foreign_key_violation.
|
|
92
93
|
if (error.code === '23503') {
|
|
93
|
-
return
|
|
94
|
+
return createPlutoError({
|
|
94
95
|
statusCode: 400,
|
|
95
|
-
|
|
96
|
+
message: 'This references a record that does not exist.',
|
|
97
|
+
key: 'supabase.errors.missingReference',
|
|
96
98
|
})
|
|
97
99
|
}
|
|
98
100
|
|
|
Binary file
|
package/server/utils/validate.ts
CHANGED
|
@@ -37,7 +37,12 @@ export async function parseBody<T>(event: H3Event, schema: ZodSchema<T>): Promis
|
|
|
37
37
|
message: issue.message,
|
|
38
38
|
}))
|
|
39
39
|
|
|
40
|
-
throw
|
|
40
|
+
throw createPlutoError({
|
|
41
|
+
statusCode: 400,
|
|
42
|
+
message: 'Validation failed.',
|
|
43
|
+
key: 'pluto.errors.validationFailed',
|
|
44
|
+
data: { errors },
|
|
45
|
+
})
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
return result.data
|
package/shared/types/supabase.ts
CHANGED
|
@@ -232,6 +232,7 @@ export type Database = {
|
|
|
232
232
|
email: string | null
|
|
233
233
|
id: string
|
|
234
234
|
is_admin: boolean
|
|
235
|
+
locale: string | null
|
|
235
236
|
updated_at: string | null
|
|
236
237
|
username: string | null
|
|
237
238
|
}
|
|
@@ -240,6 +241,7 @@ export type Database = {
|
|
|
240
241
|
email?: string | null
|
|
241
242
|
id: string
|
|
242
243
|
is_admin?: boolean
|
|
244
|
+
locale?: string | null
|
|
243
245
|
updated_at?: string | null
|
|
244
246
|
username?: string | null
|
|
245
247
|
}
|
|
@@ -248,6 +250,7 @@ export type Database = {
|
|
|
248
250
|
email?: string | null
|
|
249
251
|
id?: string
|
|
250
252
|
is_admin?: boolean
|
|
253
|
+
locale?: string | null
|
|
251
254
|
updated_at?: string | null
|
|
252
255
|
username?: string | null
|
|
253
256
|
}
|
|
@@ -364,6 +367,10 @@ export type Database = {
|
|
|
364
367
|
Args: Record<PropertyKey, never>
|
|
365
368
|
Returns: boolean
|
|
366
369
|
}
|
|
370
|
+
set_my_locale: {
|
|
371
|
+
Args: { new_locale: string }
|
|
372
|
+
Returns: undefined
|
|
373
|
+
}
|
|
367
374
|
}
|
|
368
375
|
Enums: {
|
|
369
376
|
thealthcheck: "first_setup"
|