@plutocms/supabase 0.10.0 → 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/CHANGELOG.md +11 -0
- package/FEATURES.md +3 -0
- package/README.md +10 -1
- package/app/components/auth/AuthSignIn.vue +56 -0
- package/app/components/migrations/MigrationsBanner.vue +26 -12
- package/app/composables/auth.ts +22 -88
- package/app/pages/admin/(auth)/confirm.vue +17 -21
- package/app/pages/admin/(auth)/forgot-password.vue +57 -64
- package/app/pages/admin/(auth)/signup.vue +104 -107
- package/app/pages/admin/(auth)/update-password.vue +75 -82
- package/app/pages/admin/setup.vue +0 -1
- package/app/plugins/pluto-extension.ts +119 -7
- package/app/utils/auth-driver.ts +110 -0
- package/i18n/locales/en.json +2 -18
- package/i18n/locales/pt-BR.json +2 -18
- package/package.json +2 -2
- package/app/components/navbar/NavbarAdmin.vue +0 -219
- 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 -95
- package/app/pages/admin.vue +0 -57
- package/app/plugins/pluto-locale-sync.client.ts +0 -30
- package/app/plugins/pluto-permissions-sync.ts +0 -38
package/i18n/locales/en.json
CHANGED
|
@@ -28,12 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"toasts": {
|
|
30
30
|
"loginFailed": "Could not log in",
|
|
31
|
-
"loggedOut": "Logged out successfully",
|
|
32
31
|
"resetFailed": "Could not send reset email",
|
|
33
32
|
"updateFailed": "Could not update password",
|
|
34
|
-
"passwordUpdated": "Password updated successfully"
|
|
35
|
-
"notLoggedIn": "You are not logged in",
|
|
36
|
-
"notLoggedInDescription": "Please log in to continue."
|
|
33
|
+
"passwordUpdated": "Password updated successfully"
|
|
37
34
|
},
|
|
38
35
|
"errors": {
|
|
39
36
|
"unknown": "An unknown error occurred.",
|
|
@@ -65,10 +62,7 @@
|
|
|
65
62
|
},
|
|
66
63
|
"login": {
|
|
67
64
|
"title": "Login",
|
|
68
|
-
"submit": "Login"
|
|
69
|
-
"createAccount": "Create Account",
|
|
70
|
-
"backToHome": "Back to Home",
|
|
71
|
-
"forgotPassword": "Forgot password?"
|
|
65
|
+
"submit": "Login"
|
|
72
66
|
},
|
|
73
67
|
"update": {
|
|
74
68
|
"title": "Update Password",
|
|
@@ -95,16 +89,6 @@
|
|
|
95
89
|
"checkSpam": "If you don't see the email, please check your spam folder."
|
|
96
90
|
}
|
|
97
91
|
},
|
|
98
|
-
"navbar": {
|
|
99
|
-
"defaultUserName": "User",
|
|
100
|
-
"github": "Pluto on GitHub",
|
|
101
|
-
"logout": "Logout",
|
|
102
|
-
"openNavigation": "Open navigation",
|
|
103
|
-
"goHome": "Go to dashboard home",
|
|
104
|
-
"visitSite": "Visit {domain}",
|
|
105
|
-
"viewSite": "View site",
|
|
106
|
-
"openUserMenu": "Open user menu"
|
|
107
|
-
},
|
|
108
92
|
"settings": {
|
|
109
93
|
"general": {
|
|
110
94
|
"title": "General",
|
package/i18n/locales/pt-BR.json
CHANGED
|
@@ -28,12 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"toasts": {
|
|
30
30
|
"loginFailed": "Não foi possível entrar",
|
|
31
|
-
"loggedOut": "Sessão encerrada com sucesso",
|
|
32
31
|
"resetFailed": "Não foi possível enviar o e-mail de redefinição",
|
|
33
32
|
"updateFailed": "Não foi possível atualizar a senha",
|
|
34
|
-
"passwordUpdated": "Senha atualizada com sucesso"
|
|
35
|
-
"notLoggedIn": "Você não está conectado",
|
|
36
|
-
"notLoggedInDescription": "Entre na sua conta para continuar."
|
|
33
|
+
"passwordUpdated": "Senha atualizada com sucesso"
|
|
37
34
|
},
|
|
38
35
|
"errors": {
|
|
39
36
|
"unknown": "Ocorreu um erro desconhecido.",
|
|
@@ -65,10 +62,7 @@
|
|
|
65
62
|
},
|
|
66
63
|
"login": {
|
|
67
64
|
"title": "Entrar",
|
|
68
|
-
"submit": "Entrar"
|
|
69
|
-
"createAccount": "Criar conta",
|
|
70
|
-
"backToHome": "Voltar para o início",
|
|
71
|
-
"forgotPassword": "Esqueceu a senha?"
|
|
65
|
+
"submit": "Entrar"
|
|
72
66
|
},
|
|
73
67
|
"update": {
|
|
74
68
|
"title": "Atualizar senha",
|
|
@@ -95,16 +89,6 @@
|
|
|
95
89
|
"checkSpam": "Se você não encontrar o e-mail, verifique sua pasta de spam."
|
|
96
90
|
}
|
|
97
91
|
},
|
|
98
|
-
"navbar": {
|
|
99
|
-
"defaultUserName": "Usuário",
|
|
100
|
-
"github": "Pluto no GitHub",
|
|
101
|
-
"logout": "Sair",
|
|
102
|
-
"openNavigation": "Abrir navegação",
|
|
103
|
-
"goHome": "Ir para o início do painel",
|
|
104
|
-
"visitSite": "Visitar {domain}",
|
|
105
|
-
"viewSite": "Ver site",
|
|
106
|
-
"openUserMenu": "Abrir menu do usuário"
|
|
107
|
-
},
|
|
108
92
|
"settings": {
|
|
109
93
|
"general": {
|
|
110
94
|
"title": "Geral",
|
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",
|
|
@@ -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",
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { UDropdownMenu } from '#components'
|
|
3
|
-
import { domainFromUrl } from '#imports'
|
|
4
|
-
|
|
5
|
-
const host = useRequestURL().host
|
|
6
|
-
|
|
7
|
-
const route = useRoute()
|
|
8
|
-
const isSidebarOpen = useState<boolean>('pluto-admin-sidebar-open', () => false)
|
|
9
|
-
|
|
10
|
-
function openSidebar() {
|
|
11
|
-
isSidebarOpen.value = true
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const { t } = usePlutoI18n()
|
|
15
|
-
|
|
16
|
-
const { isLoggedIn, user, logout } = await useAuth()
|
|
17
|
-
|
|
18
|
-
const displayName = computed(() => {
|
|
19
|
-
return (
|
|
20
|
-
user.value?.display_name ||
|
|
21
|
-
user.value?.full_name ||
|
|
22
|
-
user.value?.name ||
|
|
23
|
-
user.value?.username ||
|
|
24
|
-
t('supabase.navbar.defaultUserName')
|
|
25
|
-
)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
const shortDisplayName = computed(() => displayName.value.split(' ')[0])
|
|
29
|
-
|
|
30
|
-
const userMenuLabel = computed(() => {
|
|
31
|
-
return user.value?.username
|
|
32
|
-
? `${displayName.value} (${user.value.username})`
|
|
33
|
-
: displayName.value
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
const has_settings_modified = useState<boolean>('has_settings_modified')
|
|
37
|
-
|
|
38
|
-
const { data: settingsData, status } = await useFetch('/api/settings', {
|
|
39
|
-
watch: [has_settings_modified],
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
const maybeDevUrl = computed(() => {
|
|
43
|
-
if (import.meta.dev) {
|
|
44
|
-
return `http://${host}`
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return settingsData.value?.settings.website_url || ''
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
type DropdownItems = NonNullable<
|
|
51
|
-
Parameters<typeof UDropdownMenu>[0]['items']
|
|
52
|
-
>
|
|
53
|
-
|
|
54
|
-
const items = computed<DropdownItems>(() => [
|
|
55
|
-
[
|
|
56
|
-
{
|
|
57
|
-
label: userMenuLabel.value,
|
|
58
|
-
avatar: {
|
|
59
|
-
icon: 'lucide:user',
|
|
60
|
-
class: 'bg-primary',
|
|
61
|
-
ui: {
|
|
62
|
-
icon: 'text-inverted text-sm',
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
type: 'label',
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
|
|
69
|
-
[
|
|
70
|
-
{
|
|
71
|
-
label: t('pluto.common.settings'),
|
|
72
|
-
icon: 'i-lucide-cog',
|
|
73
|
-
type: 'link',
|
|
74
|
-
to: '/admin/settings',
|
|
75
|
-
kbds: ['meta', ','],
|
|
76
|
-
onSelect() {
|
|
77
|
-
if (isLoggedIn.value) {
|
|
78
|
-
navigateTo('/admin/settings')
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
|
|
84
|
-
[
|
|
85
|
-
{
|
|
86
|
-
label: t('supabase.navbar.github'),
|
|
87
|
-
icon: 'i-simple-icons-github',
|
|
88
|
-
type: 'link',
|
|
89
|
-
to: 'https://github.com/ojvribeiro/pluto',
|
|
90
|
-
target: '_blank',
|
|
91
|
-
external: true,
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
|
|
95
|
-
[
|
|
96
|
-
{
|
|
97
|
-
label: t('supabase.navbar.logout'),
|
|
98
|
-
icon: 'i-lucide-log-out',
|
|
99
|
-
kbds: ['meta', 'shift', 'q'],
|
|
100
|
-
class: 'cursor-pointer',
|
|
101
|
-
onSelect() {
|
|
102
|
-
logout({ showToast: true })
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
])
|
|
107
|
-
|
|
108
|
-
defineShortcuts(extractShortcuts(items.value))
|
|
109
|
-
</script>
|
|
110
|
-
|
|
111
|
-
<template>
|
|
112
|
-
<PlutoNavbarAdmin>
|
|
113
|
-
<div
|
|
114
|
-
class="flex h-full min-w-0 items-stretch justify-between gap-1 px-2 lg:px-4"
|
|
115
|
-
>
|
|
116
|
-
<div class="flex min-w-0 items-center gap-x-1 lg:gap-x-3">
|
|
117
|
-
<UButton
|
|
118
|
-
:aria-label="t('supabase.navbar.openNavigation')"
|
|
119
|
-
class="lg:hidden"
|
|
120
|
-
icon="lucide:menu"
|
|
121
|
-
color="neutral"
|
|
122
|
-
variant="ghost"
|
|
123
|
-
square
|
|
124
|
-
@click="openSidebar"
|
|
125
|
-
/>
|
|
126
|
-
|
|
127
|
-
<div class="h-full shrink-0 py-1">
|
|
128
|
-
<NuxtLink
|
|
129
|
-
:title="t('supabase.navbar.goHome')"
|
|
130
|
-
to="/admin/home"
|
|
131
|
-
class="group block h-full"
|
|
132
|
-
>
|
|
133
|
-
<img
|
|
134
|
-
src="/img/pluto.png"
|
|
135
|
-
class="block h-full grayscale-100 group-hover:grayscale-0"
|
|
136
|
-
/>
|
|
137
|
-
</NuxtLink>
|
|
138
|
-
</div>
|
|
139
|
-
|
|
140
|
-
<div class="min-w-0 h-full overflow-hidden">
|
|
141
|
-
<ul class="flex h-full min-w-0 items-center text-sm">
|
|
142
|
-
<NavbarAdminActionButton
|
|
143
|
-
:show="route.path.startsWith('/admin')"
|
|
144
|
-
:title="
|
|
145
|
-
t('supabase.navbar.visitSite', {
|
|
146
|
-
domain: domainFromUrl(maybeDevUrl),
|
|
147
|
-
})
|
|
148
|
-
"
|
|
149
|
-
:icon="
|
|
150
|
-
status === 'pending'
|
|
151
|
-
? 'lucide:loader-circle'
|
|
152
|
-
: 'lucide:external-link'
|
|
153
|
-
"
|
|
154
|
-
:disabled="status === 'pending'"
|
|
155
|
-
:to="maybeDevUrl"
|
|
156
|
-
:label="t('supabase.navbar.viewSite')"
|
|
157
|
-
target="_blank"
|
|
158
|
-
/>
|
|
159
|
-
|
|
160
|
-
<ClientOnly>
|
|
161
|
-
<template #fallback>
|
|
162
|
-
<li class="h-full">
|
|
163
|
-
<div
|
|
164
|
-
class="text-dimmed flex h-full items-center gap-x-2 rounded-sm px-2"
|
|
165
|
-
>
|
|
166
|
-
<Icon name="svg-spinners:ring-resize" />
|
|
167
|
-
</div>
|
|
168
|
-
</li>
|
|
169
|
-
</template>
|
|
170
|
-
|
|
171
|
-
<PlutoNavbarAdminActions />
|
|
172
|
-
</ClientOnly>
|
|
173
|
-
</ul>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
|
|
177
|
-
<div class="flex shrink-0 items-center gap-1 lg:gap-3">
|
|
178
|
-
<ColorModeButton />
|
|
179
|
-
|
|
180
|
-
<div class="h-full">
|
|
181
|
-
<UDropdownMenu
|
|
182
|
-
:items="items"
|
|
183
|
-
:ui="{
|
|
184
|
-
content: 'w-64',
|
|
185
|
-
}"
|
|
186
|
-
:modal="false"
|
|
187
|
-
>
|
|
188
|
-
<button
|
|
189
|
-
:aria-label="t('supabase.navbar.openUserMenu')"
|
|
190
|
-
class="group h-full py-1"
|
|
191
|
-
type="button"
|
|
192
|
-
>
|
|
193
|
-
<div
|
|
194
|
-
class="flex h-full items-center gap-x-2 rounded-sm rounded-l-xl pr-2 pl-0.5 group-hover:bg-elevated"
|
|
195
|
-
>
|
|
196
|
-
<UAvatar
|
|
197
|
-
:ui="{
|
|
198
|
-
root: 'rounded-xl',
|
|
199
|
-
icon: 'text-inverted text-lg',
|
|
200
|
-
}"
|
|
201
|
-
:alt="displayName"
|
|
202
|
-
size="sm"
|
|
203
|
-
icon="lucide:user"
|
|
204
|
-
class="bg-primary"
|
|
205
|
-
/>
|
|
206
|
-
|
|
207
|
-
<span
|
|
208
|
-
class="text-highlighted hidden text-sm font-semibold lg:inline"
|
|
209
|
-
>
|
|
210
|
-
{{ shortDisplayName }}
|
|
211
|
-
</span>
|
|
212
|
-
</div>
|
|
213
|
-
</button>
|
|
214
|
-
</UDropdownMenu>
|
|
215
|
-
</div>
|
|
216
|
-
</div>
|
|
217
|
-
</div>
|
|
218
|
-
</PlutoNavbarAdmin>
|
|
219
|
-
</template>
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
const route = useRoute()
|
|
3
|
-
const session = useSupabaseSession()
|
|
4
|
-
|
|
5
|
-
const shouldShowNavbar = computed(() => {
|
|
6
|
-
if (allowedUnauthenticatedPaths.includes(route.path)) {
|
|
7
|
-
return false
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
return route.path === '/admin'
|
|
11
|
-
|| route.path.startsWith('/admin/')
|
|
12
|
-
|| Boolean(session.value)
|
|
13
|
-
})
|
|
14
|
-
</script>
|
|
15
|
-
|
|
16
|
-
<template>
|
|
17
|
-
<NavbarAdmin v-if="shouldShowNavbar" />
|
|
18
|
-
</template>
|
package/app/middleware/auth.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { RouteLocationNormalizedGeneric } from 'vue-router'
|
|
2
|
-
|
|
3
|
-
export default defineNuxtRouteMiddleware(async (to, from) => {
|
|
4
|
-
const { isLoggedIn, logout, allowedUnauthenticatedPaths } = await useAuth({
|
|
5
|
-
route: to as RouteLocationNormalizedGeneric,
|
|
6
|
-
})
|
|
7
|
-
|
|
8
|
-
if (
|
|
9
|
-
isLoggedIn.value &&
|
|
10
|
-
allowedUnauthenticatedPaths.includes(to.path) &&
|
|
11
|
-
to.path !== '/admin/update-password'
|
|
12
|
-
) {
|
|
13
|
-
return navigateTo(
|
|
14
|
-
to.query.redirect
|
|
15
|
-
? decodeURIComponent(to.query.redirect as string)
|
|
16
|
-
: '/admin/home'
|
|
17
|
-
)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
!isLoggedIn.value &&
|
|
22
|
-
to.path.startsWith('/admin') &&
|
|
23
|
-
!allowedUnauthenticatedPaths.includes(to.path)
|
|
24
|
-
) {
|
|
25
|
-
if (from.query.redirect) {
|
|
26
|
-
return logout({
|
|
27
|
-
redirectTo: from.query.redirect as string,
|
|
28
|
-
showToast: true,
|
|
29
|
-
})
|
|
30
|
-
} else {
|
|
31
|
-
return logout()
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
})
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
export default defineNuxtRouteMiddleware(async (to, _from) => {
|
|
2
|
-
if (!to.path.startsWith('/admin')) {
|
|
3
|
-
return
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
try {
|
|
7
|
-
const data = await $fetch('/api/settings/first_setup')
|
|
8
|
-
|
|
9
|
-
if (!data.success) {
|
|
10
|
-
// Cannot determine setup state — send to setup page
|
|
11
|
-
if (to.path !== '/admin/setup') {
|
|
12
|
-
return navigateTo('/admin/setup')
|
|
13
|
-
}
|
|
14
|
-
return
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (data.is_first_setup) {
|
|
18
|
-
// Setup not done yet — only allow the setup page
|
|
19
|
-
if (to.path !== '/admin/setup') {
|
|
20
|
-
return navigateTo('/admin/setup')
|
|
21
|
-
}
|
|
22
|
-
} else {
|
|
23
|
-
// Setup already done — block access to setup page.
|
|
24
|
-
//
|
|
25
|
-
// Keep this redirect even after layer migrations move to
|
|
26
|
-
// /admin/migrations. The setup page prompts an unauthenticated
|
|
27
|
-
// visitor for a raw database connection string, and it is safe only
|
|
28
|
-
// because it is unreachable once setup is done. Reopening it for a
|
|
29
|
-
// routine layer addition would let an anonymous visitor submit a
|
|
30
|
-
// connection string to the server.
|
|
31
|
-
if (to.path === '/admin/setup') {
|
|
32
|
-
return navigateTo('/admin/login')
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
} catch (error) {
|
|
36
|
-
if (import.meta.dev) {
|
|
37
|
-
console.error('Error checking first setup status:', error)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (to.path !== '/admin/setup') {
|
|
41
|
-
return navigateTo('/admin/setup')
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
})
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
interface Form {
|
|
3
|
-
email: string
|
|
4
|
-
password: string
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
definePageMeta({
|
|
8
|
-
layout: 'auth',
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
const { t } = usePlutoI18n()
|
|
12
|
-
|
|
13
|
-
useHead({
|
|
14
|
-
title: () => t('supabase.auth.login.title'),
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
const { isSubmitting, login } = await useAuth()
|
|
18
|
-
|
|
19
|
-
const form = ref<Form>({
|
|
20
|
-
email: '',
|
|
21
|
-
password: '',
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
function submitForm() {
|
|
25
|
-
login(form.value)
|
|
26
|
-
}
|
|
27
|
-
</script>
|
|
28
|
-
|
|
29
|
-
<template>
|
|
30
|
-
<div class="grid min-h-dvh place-items-center p-4">
|
|
31
|
-
<UCard class="w-full max-w-100">
|
|
32
|
-
<form @submit.prevent="submitForm">
|
|
33
|
-
<div class="flex flex-col gap-y-6">
|
|
34
|
-
<h1 class="text-3xl font-bold">{{ t('supabase.auth.login.title') }}</h1>
|
|
35
|
-
|
|
36
|
-
<UFormField :label="t('supabase.auth.fields.email')">
|
|
37
|
-
<UInput
|
|
38
|
-
v-model="form.email"
|
|
39
|
-
placeholder="victor@example.com"
|
|
40
|
-
class="w-full"
|
|
41
|
-
autocomplete="email"
|
|
42
|
-
/>
|
|
43
|
-
</UFormField>
|
|
44
|
-
|
|
45
|
-
<UFormField :label="t('supabase.auth.fields.password')">
|
|
46
|
-
<UInput
|
|
47
|
-
v-model="form.password"
|
|
48
|
-
placeholder="••••"
|
|
49
|
-
class="w-full"
|
|
50
|
-
type="password"
|
|
51
|
-
/>
|
|
52
|
-
</UFormField>
|
|
53
|
-
|
|
54
|
-
<div
|
|
55
|
-
class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-x-4"
|
|
56
|
-
>
|
|
57
|
-
<UFormField>
|
|
58
|
-
<UButton
|
|
59
|
-
:loading="isSubmitting"
|
|
60
|
-
type="submit"
|
|
61
|
-
icon="lucide:log-in"
|
|
62
|
-
class="w-full justify-center sm:w-auto"
|
|
63
|
-
>
|
|
64
|
-
{{ t('supabase.auth.login.submit') }}
|
|
65
|
-
</UButton>
|
|
66
|
-
</UFormField>
|
|
67
|
-
|
|
68
|
-
<UFormField>
|
|
69
|
-
<UButton variant="link" to="/admin/signup" class="px-0">
|
|
70
|
-
{{ t('supabase.auth.login.createAccount') }}
|
|
71
|
-
</UButton>
|
|
72
|
-
</UFormField>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<div
|
|
76
|
-
class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"
|
|
77
|
-
>
|
|
78
|
-
<UButton
|
|
79
|
-
variant="link"
|
|
80
|
-
icon="lucide:arrow-left"
|
|
81
|
-
to="/"
|
|
82
|
-
class="px-0"
|
|
83
|
-
>
|
|
84
|
-
{{ t('supabase.auth.login.backToHome') }}
|
|
85
|
-
</UButton>
|
|
86
|
-
|
|
87
|
-
<UButton variant="link" to="/admin/forgot-password" class="px-0">
|
|
88
|
-
{{ t('supabase.auth.login.forgotPassword') }}
|
|
89
|
-
</UButton>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
</form>
|
|
93
|
-
</UCard>
|
|
94
|
-
</div>
|
|
95
|
-
</template>
|
package/app/pages/admin.vue
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
definePageMeta({
|
|
3
|
-
middleware: ['setup-check', 'auth'],
|
|
4
|
-
layout: 'admin',
|
|
5
|
-
})
|
|
6
|
-
|
|
7
|
-
const route = useRoute()
|
|
8
|
-
const { t } = usePlutoI18n()
|
|
9
|
-
const { isLoggedIn, logout, allowedUnauthenticatedPaths } = await useAuth()
|
|
10
|
-
const toast = useToast()
|
|
11
|
-
|
|
12
|
-
// Loading/clearing permissions on login/logout lives in
|
|
13
|
-
// app/plugins/pluto-permissions-sync.ts now, not here — that plugin runs
|
|
14
|
-
// on every page, not just this one, so a signed-in admin browsing a public
|
|
15
|
-
// page directly (no prior visit to /admin/** this session) still gets
|
|
16
|
-
// their capabilities loaded.
|
|
17
|
-
|
|
18
|
-
const visibility = useDocumentVisibility()
|
|
19
|
-
|
|
20
|
-
watch(visibility, async (current, previous) => {
|
|
21
|
-
if (current === 'visible' && previous === 'hidden') {
|
|
22
|
-
if (
|
|
23
|
-
!isLoggedIn.value &&
|
|
24
|
-
!route.path.startsWith('/admin/setup') &&
|
|
25
|
-
!allowedUnauthenticatedPaths.includes(route.path)
|
|
26
|
-
) {
|
|
27
|
-
await logout({ redirectTo: route.path })
|
|
28
|
-
|
|
29
|
-
toast.add({
|
|
30
|
-
title: t('supabase.auth.toasts.notLoggedIn'),
|
|
31
|
-
description: t('supabase.auth.toasts.notLoggedInDescription'),
|
|
32
|
-
icon: 'lucide:circle-x',
|
|
33
|
-
color: 'error',
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
</script>
|
|
39
|
-
|
|
40
|
-
<template>
|
|
41
|
-
<div class="light:text-zinc-800 h-full dark:text-white">
|
|
42
|
-
<ClientOnly>
|
|
43
|
-
<Container
|
|
44
|
-
v-if="
|
|
45
|
-
isLoggedIn
|
|
46
|
-
&& !route.path.startsWith('/admin/setup')
|
|
47
|
-
&& !route.path.startsWith('/admin/migrations')
|
|
48
|
-
"
|
|
49
|
-
class="pt-4"
|
|
50
|
-
>
|
|
51
|
-
<MigrationsBanner />
|
|
52
|
-
</Container>
|
|
53
|
-
</ClientOnly>
|
|
54
|
-
|
|
55
|
-
<NuxtPage />
|
|
56
|
-
</div>
|
|
57
|
-
</template>
|
|
@@ -1,30 +0,0 @@
|
|
|
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
|
-
})
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Loads (or clears) the current user's capabilities whenever their session
|
|
3
|
-
* appears or disappears — app-wide, not page-scoped.
|
|
4
|
-
*
|
|
5
|
-
* This used to live as a `watch` inside `app/pages/admin.vue`. That only
|
|
6
|
-
* ran while `admin.vue` was the mounted page, so a signed-in admin loading
|
|
7
|
-
* a public page directly (a fresh navigation or a page reload, with no
|
|
8
|
-
* earlier visit to `/admin/**` in the same app instance) never triggered a
|
|
9
|
-
* load at all — `usePlutoPermissions().granted` stayed empty, and every
|
|
10
|
-
* `can()` check for that admin read as `false` on that page, even though
|
|
11
|
-
* the same admin's capability-gated UI worked correctly inside `/admin/**`.
|
|
12
|
-
* `NavbarAdminActions.vue`'s "Edit product" quick-link (shown while
|
|
13
|
-
* viewing a live `/product/**` page) is exactly the kind of capability-
|
|
14
|
-
* gated UI that lives outside `/admin/**` by design, so it needs this to
|
|
15
|
-
* fire on every page, not just admin ones.
|
|
16
|
-
*
|
|
17
|
-
* `useSupabaseSession()` (not `useAuth()`) on purpose: it is the same
|
|
18
|
-
* primitive `useAuth()`'s own `isLoggedIn` is built from
|
|
19
|
-
* (`computed(() => !!supabaseSession.value)`), and it is genuinely
|
|
20
|
-
* app-wide reactive state already (managed by `@nuxtjs/supabase`'s own
|
|
21
|
-
* plugin), so reading it directly here needs no extra composable surface.
|
|
22
|
-
*/
|
|
23
|
-
export default defineNuxtPlugin(() => {
|
|
24
|
-
const session = useSupabaseSession()
|
|
25
|
-
const { load, clear } = usePlutoPermissions()
|
|
26
|
-
|
|
27
|
-
watch(
|
|
28
|
-
session,
|
|
29
|
-
(current) => {
|
|
30
|
-
if (current) {
|
|
31
|
-
load()
|
|
32
|
-
} else {
|
|
33
|
-
clear()
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
{ immediate: true }
|
|
37
|
-
)
|
|
38
|
-
})
|