@plutocms/supabase 0.18.0 → 0.19.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 +14 -0
- package/FEATURES.md +1 -1
- package/README.md +1 -1
- package/app/pages/admin/roles.vue +256 -0
- package/app/pages/admin/users.vue +473 -0
- package/app/plugins/pluto-extension.ts +23 -15
- package/nuxt.config.ts +3 -10
- package/package.json +4 -13
- package/app/pages/admin/settings/roles.vue +0 -230
- package/app/pages/admin/settings/users.vue +0 -447
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The Users page (`/admin/users`, the Users item of the admin menu). It lists
|
|
3
|
+
// the staff (users with a role). An admin invites users, changes their roles,
|
|
4
|
+
// resends an invite, copies an invite link and revokes access. It needs
|
|
5
|
+
// `users:read`: without it, the page shows an alert and loads nothing. The API
|
|
6
|
+
// routes do their own check. See the users-and-roles skill.
|
|
7
|
+
import type { DropdownMenuItem, TableColumn } from '@nuxt/ui'
|
|
8
|
+
import type {
|
|
9
|
+
AccessCapabilities,
|
|
10
|
+
AccessRole,
|
|
11
|
+
AccessUser,
|
|
12
|
+
} from '../../../shared/types/access'
|
|
13
|
+
import {
|
|
14
|
+
isAdminUser,
|
|
15
|
+
roleLabelOf,
|
|
16
|
+
userDisplayName,
|
|
17
|
+
userStatus,
|
|
18
|
+
} from '../../utils/access-ui'
|
|
19
|
+
|
|
20
|
+
// The modals and the slideover load on demand. They stay out of the page chunk.
|
|
21
|
+
const InviteModal = defineAsyncComponent(
|
|
22
|
+
() => import('../../components/access/InviteModal.vue')
|
|
23
|
+
)
|
|
24
|
+
const EditRolesSlideover = defineAsyncComponent(
|
|
25
|
+
() => import('../../components/access/EditRolesSlideover.vue')
|
|
26
|
+
)
|
|
27
|
+
const RevokeModal = defineAsyncComponent(
|
|
28
|
+
() => import('../../components/access/RevokeModal.vue')
|
|
29
|
+
)
|
|
30
|
+
const LinkModal = defineAsyncComponent(
|
|
31
|
+
() => import('../../components/access/LinkModal.vue')
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const { t, d, locale } = usePlutoI18n()
|
|
35
|
+
const toast = useToast()
|
|
36
|
+
const overlay = useOverlay()
|
|
37
|
+
const errorText = usePlutoErrorText()
|
|
38
|
+
const permissions = usePlutoPermissions()
|
|
39
|
+
const auth = usePlutoAuth()
|
|
40
|
+
|
|
41
|
+
useHead({
|
|
42
|
+
title: () => t('supabase.access.users.title'),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const allowed = computed(() => permissions.can('users:read'))
|
|
46
|
+
|
|
47
|
+
const users = ref<AccessUser[]>([])
|
|
48
|
+
const roles = ref<AccessRole[]>([])
|
|
49
|
+
const status = ref<'loading' | 'ready' | 'error'>('loading')
|
|
50
|
+
const loadError = ref('')
|
|
51
|
+
const capabilities = ref<AccessCapabilities | null>(null)
|
|
52
|
+
|
|
53
|
+
const isAdmin = computed(() => permissions.isAdmin.value)
|
|
54
|
+
const selfId = computed(() => auth.user.value?.id ?? '')
|
|
55
|
+
|
|
56
|
+
async function load() {
|
|
57
|
+
// Keep the table on screen while it reloads after a change.
|
|
58
|
+
if (status.value !== 'ready') {
|
|
59
|
+
status.value = 'loading'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const [usersResponse, rolesResponse] = await Promise.all([
|
|
64
|
+
$fetch<{ users: AccessUser[] }>('/api/_pluto/supabase/access/users'),
|
|
65
|
+
$fetch<{ roles: AccessRole[] }>('/api/_pluto/supabase/access/roles'),
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
users.value = usersResponse.users
|
|
69
|
+
roles.value = rolesResponse.roles
|
|
70
|
+
status.value = 'ready'
|
|
71
|
+
} catch (error) {
|
|
72
|
+
loadError.value = errorText(error)
|
|
73
|
+
status.value = 'error'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function loadCapabilities() {
|
|
78
|
+
try {
|
|
79
|
+
capabilities.value = await $fetch<AccessCapabilities>(
|
|
80
|
+
'/api/_pluto/supabase/access/capabilities'
|
|
81
|
+
)
|
|
82
|
+
} catch {
|
|
83
|
+
// The invite works without it. Only the "Copy invite link" action hides.
|
|
84
|
+
capabilities.value = null
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
watch(
|
|
89
|
+
allowed,
|
|
90
|
+
(ok) => {
|
|
91
|
+
if (ok) {
|
|
92
|
+
load()
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{ immediate: true }
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// Only an admin can read this, so ask after the permissions are known.
|
|
99
|
+
watch(
|
|
100
|
+
isAdmin,
|
|
101
|
+
(admin) => {
|
|
102
|
+
if (admin && !capabilities.value) {
|
|
103
|
+
void loadCapabilities()
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{ immediate: true }
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const columns = computed<TableColumn<AccessUser>[]>(() => [
|
|
110
|
+
{ id: 'user', header: t('supabase.access.users.columns.user') },
|
|
111
|
+
{ id: 'roles', header: t('supabase.access.users.columns.roles') },
|
|
112
|
+
{ id: 'status', header: t('supabase.access.users.columns.status') },
|
|
113
|
+
{ id: 'lastSignIn', header: t('supabase.access.users.columns.lastSignIn') },
|
|
114
|
+
{ id: 'actions', header: '' },
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
function dateText(
|
|
118
|
+
value: string | null,
|
|
119
|
+
options: Intl.DateTimeFormatOptions
|
|
120
|
+
): string {
|
|
121
|
+
return value ? d(new Date(value), options) : ''
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// --- Invite ---
|
|
125
|
+
const inviteMounted = ref(false)
|
|
126
|
+
const inviteOpen = ref(false)
|
|
127
|
+
|
|
128
|
+
function openInvite() {
|
|
129
|
+
inviteMounted.value = true
|
|
130
|
+
inviteOpen.value = true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- Edit roles ---
|
|
134
|
+
const editMounted = ref(false)
|
|
135
|
+
const editOpen = ref(false)
|
|
136
|
+
const editUser = ref<AccessUser | null>(null)
|
|
137
|
+
|
|
138
|
+
function openEdit(user: AccessUser) {
|
|
139
|
+
editUser.value = user
|
|
140
|
+
editMounted.value = true
|
|
141
|
+
editOpen.value = true
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// --- Revoke ---
|
|
145
|
+
async function openRevoke(user: AccessUser) {
|
|
146
|
+
const modal = overlay.create(RevokeModal)
|
|
147
|
+
const revoked = await modal.open({ user, self: user.id === selfId.value })
|
|
148
|
+
|
|
149
|
+
if (revoked) {
|
|
150
|
+
await load()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- Invite link ---
|
|
155
|
+
async function showLink(link: string) {
|
|
156
|
+
await overlay.create(LinkModal).open({ link })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function copyLink(user: AccessUser) {
|
|
160
|
+
if (!user.email) {
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const result = await $fetch<{ link: string | null }>(
|
|
166
|
+
'/api/_pluto/supabase/access/users/invite-link',
|
|
167
|
+
{
|
|
168
|
+
method: 'POST',
|
|
169
|
+
body: { email: user.email, roleKeys: user.roles },
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
// A confirmed account gets no link: it signs in with its own password.
|
|
174
|
+
if (!result.link) {
|
|
175
|
+
toast.add({
|
|
176
|
+
title: t('supabase.access.toasts.noLinkConfirmed'),
|
|
177
|
+
color: 'info',
|
|
178
|
+
icon: 'lucide:info',
|
|
179
|
+
})
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await showLink(result.link)
|
|
184
|
+
} catch (error) {
|
|
185
|
+
toast.add({
|
|
186
|
+
title: t('supabase.access.toasts.failed'),
|
|
187
|
+
description: errorText(error),
|
|
188
|
+
color: 'error',
|
|
189
|
+
icon: 'lucide:circle-x',
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- Resend ---
|
|
195
|
+
async function resend(user: AccessUser) {
|
|
196
|
+
try {
|
|
197
|
+
const result = await $fetch<{ emailSent?: boolean; link?: string }>(
|
|
198
|
+
`/api/_pluto/supabase/access/users/${user.id}/resend`,
|
|
199
|
+
{ method: 'POST', body: { locale: locale.value } }
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if (result.emailSent === false && result.link) {
|
|
203
|
+
toast.add({
|
|
204
|
+
title: t('supabase.access.toasts.resentNoEmail'),
|
|
205
|
+
color: 'warning',
|
|
206
|
+
icon: 'lucide:triangle-alert',
|
|
207
|
+
})
|
|
208
|
+
await showLink(result.link)
|
|
209
|
+
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
toast.add({
|
|
214
|
+
title: t('supabase.access.toasts.resent', { email: user.email ?? '' }),
|
|
215
|
+
color: 'success',
|
|
216
|
+
icon: 'lucide:circle-check',
|
|
217
|
+
})
|
|
218
|
+
} catch (error) {
|
|
219
|
+
toast.add({
|
|
220
|
+
title: t('supabase.access.toasts.failed'),
|
|
221
|
+
description: errorText(error),
|
|
222
|
+
color: 'error',
|
|
223
|
+
icon: 'lucide:circle-x',
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function menuItems(user: AccessUser): DropdownMenuItem[][] {
|
|
229
|
+
const pending = userStatus(user) === 'invited'
|
|
230
|
+
const main: DropdownMenuItem[] = [
|
|
231
|
+
{
|
|
232
|
+
label: t('supabase.access.users.menu.editRoles'),
|
|
233
|
+
icon: 'lucide:shield',
|
|
234
|
+
onSelect: () => openEdit(user),
|
|
235
|
+
},
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
if (pending) {
|
|
239
|
+
main.push({
|
|
240
|
+
label: t('supabase.access.users.menu.resend'),
|
|
241
|
+
icon: 'lucide:send',
|
|
242
|
+
onSelect: () => resend(user),
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
if (capabilities.value?.serviceKey && user.roles.length > 0) {
|
|
246
|
+
main.push({
|
|
247
|
+
label: t('supabase.access.users.menu.copyLink'),
|
|
248
|
+
icon: 'lucide:link',
|
|
249
|
+
onSelect: () => copyLink(user),
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return [
|
|
255
|
+
main,
|
|
256
|
+
[
|
|
257
|
+
{
|
|
258
|
+
label: t('supabase.access.users.menu.revoke'),
|
|
259
|
+
icon: 'lucide:user-x',
|
|
260
|
+
color: 'error',
|
|
261
|
+
onSelect: () => openRevoke(user),
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
</script>
|
|
267
|
+
|
|
268
|
+
<template>
|
|
269
|
+
<main>
|
|
270
|
+
<AdminView>
|
|
271
|
+
<div class="flex flex-wrap items-start justify-between gap-4">
|
|
272
|
+
<div>
|
|
273
|
+
<h1 class="text-3xl font-bold lg:text-4xl">
|
|
274
|
+
{{ t('supabase.access.users.title') }}
|
|
275
|
+
</h1>
|
|
276
|
+
<p class="text-muted text-sm">
|
|
277
|
+
{{ t('supabase.access.users.description') }}
|
|
278
|
+
</p>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<UButton v-if="isAdmin" icon="lucide:user-plus" @click="openInvite">
|
|
282
|
+
{{ t('supabase.access.users.invite') }}
|
|
283
|
+
</UButton>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<UAlert
|
|
287
|
+
v-if="!allowed"
|
|
288
|
+
:title="t('pluto.settings.noPermission.title')"
|
|
289
|
+
:description="
|
|
290
|
+
t('pluto.settings.noPermission.description', {
|
|
291
|
+
page: t('supabase.access.users.title'),
|
|
292
|
+
})
|
|
293
|
+
"
|
|
294
|
+
icon="lucide:lock"
|
|
295
|
+
color="warning"
|
|
296
|
+
/>
|
|
297
|
+
|
|
298
|
+
<!-- Loading -->
|
|
299
|
+
<div
|
|
300
|
+
v-else-if="status === 'loading'"
|
|
301
|
+
class="flex flex-col gap-y-3"
|
|
302
|
+
aria-busy="true"
|
|
303
|
+
>
|
|
304
|
+
<USkeleton v-for="row in 4" :key="row" class="h-12 w-full" />
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<!-- Error -->
|
|
308
|
+
<UAlert
|
|
309
|
+
v-else-if="status === 'error'"
|
|
310
|
+
:title="t('supabase.access.users.error.title')"
|
|
311
|
+
:description="loadError"
|
|
312
|
+
:actions="[
|
|
313
|
+
{
|
|
314
|
+
label: t('pluto.common.tryAgain'),
|
|
315
|
+
color: 'neutral',
|
|
316
|
+
variant: 'outline',
|
|
317
|
+
onClick: load,
|
|
318
|
+
},
|
|
319
|
+
]"
|
|
320
|
+
color="error"
|
|
321
|
+
variant="subtle"
|
|
322
|
+
icon="lucide:circle-x"
|
|
323
|
+
/>
|
|
324
|
+
|
|
325
|
+
<!-- Empty -->
|
|
326
|
+
<UEmpty
|
|
327
|
+
v-else-if="users.length === 0"
|
|
328
|
+
:title="t('supabase.access.users.empty.title')"
|
|
329
|
+
:description="t('supabase.access.users.empty.description')"
|
|
330
|
+
:actions="
|
|
331
|
+
isAdmin
|
|
332
|
+
? [
|
|
333
|
+
{
|
|
334
|
+
label: t('supabase.access.users.invite'),
|
|
335
|
+
icon: 'lucide:user-plus',
|
|
336
|
+
onClick: openInvite,
|
|
337
|
+
},
|
|
338
|
+
]
|
|
339
|
+
: []
|
|
340
|
+
"
|
|
341
|
+
icon="lucide:users"
|
|
342
|
+
/>
|
|
343
|
+
|
|
344
|
+
<!-- The list -->
|
|
345
|
+
<UTable
|
|
346
|
+
v-else
|
|
347
|
+
:data="users"
|
|
348
|
+
:columns="columns"
|
|
349
|
+
class="border-default rounded-lg border"
|
|
350
|
+
>
|
|
351
|
+
<template #user-cell="{ row }">
|
|
352
|
+
<div class="flex min-w-0 flex-col">
|
|
353
|
+
<span class="flex items-center gap-2">
|
|
354
|
+
<span class="text-highlighted truncate font-medium">{{
|
|
355
|
+
userDisplayName(row.original)
|
|
356
|
+
}}</span>
|
|
357
|
+
<UBadge
|
|
358
|
+
v-if="row.original.id === selfId"
|
|
359
|
+
:label="t('supabase.access.users.you')"
|
|
360
|
+
color="primary"
|
|
361
|
+
variant="subtle"
|
|
362
|
+
size="sm"
|
|
363
|
+
/>
|
|
364
|
+
</span>
|
|
365
|
+
<span
|
|
366
|
+
v-if="row.original.email"
|
|
367
|
+
class="text-muted truncate text-sm"
|
|
368
|
+
>{{ row.original.email }}</span
|
|
369
|
+
>
|
|
370
|
+
</div>
|
|
371
|
+
</template>
|
|
372
|
+
|
|
373
|
+
<template #roles-cell="{ row }">
|
|
374
|
+
<div class="flex flex-wrap gap-1">
|
|
375
|
+
<UBadge
|
|
376
|
+
v-if="isAdminUser(row.original)"
|
|
377
|
+
:label="t('supabase.access.users.administrator')"
|
|
378
|
+
color="primary"
|
|
379
|
+
variant="subtle"
|
|
380
|
+
/>
|
|
381
|
+
<template v-else>
|
|
382
|
+
<UBadge
|
|
383
|
+
v-for="key in row.original.roles"
|
|
384
|
+
:key="key"
|
|
385
|
+
:label="roleLabelOf(roles, key)"
|
|
386
|
+
color="neutral"
|
|
387
|
+
variant="subtle"
|
|
388
|
+
/>
|
|
389
|
+
</template>
|
|
390
|
+
</div>
|
|
391
|
+
</template>
|
|
392
|
+
|
|
393
|
+
<template #status-cell="{ row }">
|
|
394
|
+
<div class="flex flex-col items-start gap-1">
|
|
395
|
+
<UBadge
|
|
396
|
+
v-if="userStatus(row.original) === 'active'"
|
|
397
|
+
:label="t('supabase.access.users.status.active')"
|
|
398
|
+
color="success"
|
|
399
|
+
variant="subtle"
|
|
400
|
+
/>
|
|
401
|
+
<template v-else>
|
|
402
|
+
<UBadge
|
|
403
|
+
:label="t('supabase.access.users.status.invited')"
|
|
404
|
+
color="warning"
|
|
405
|
+
variant="subtle"
|
|
406
|
+
/>
|
|
407
|
+
<span v-if="row.original.invited_at" class="text-muted text-xs">
|
|
408
|
+
{{
|
|
409
|
+
t('supabase.access.users.status.invitedOn', {
|
|
410
|
+
date: dateText(row.original.invited_at, {
|
|
411
|
+
dateStyle: 'medium',
|
|
412
|
+
}),
|
|
413
|
+
})
|
|
414
|
+
}}
|
|
415
|
+
</span>
|
|
416
|
+
</template>
|
|
417
|
+
</div>
|
|
418
|
+
</template>
|
|
419
|
+
|
|
420
|
+
<template #lastSignIn-cell="{ row }">
|
|
421
|
+
<span v-if="row.original.last_sign_in_at" class="text-sm">
|
|
422
|
+
{{
|
|
423
|
+
dateText(row.original.last_sign_in_at, {
|
|
424
|
+
dateStyle: 'medium',
|
|
425
|
+
timeStyle: 'short',
|
|
426
|
+
})
|
|
427
|
+
}}
|
|
428
|
+
</span>
|
|
429
|
+
<span v-else class="text-muted text-sm">{{
|
|
430
|
+
t('supabase.access.users.neverSignedIn')
|
|
431
|
+
}}</span>
|
|
432
|
+
</template>
|
|
433
|
+
|
|
434
|
+
<template #actions-cell="{ row }">
|
|
435
|
+
<div v-if="isAdmin" class="flex justify-end">
|
|
436
|
+
<UDropdownMenu
|
|
437
|
+
:items="menuItems(row.original)"
|
|
438
|
+
:content="{ align: 'end' }"
|
|
439
|
+
>
|
|
440
|
+
<UButton
|
|
441
|
+
:aria-label="
|
|
442
|
+
t('supabase.access.users.actionsLabel', {
|
|
443
|
+
name: userDisplayName(row.original),
|
|
444
|
+
})
|
|
445
|
+
"
|
|
446
|
+
icon="lucide:ellipsis"
|
|
447
|
+
color="neutral"
|
|
448
|
+
variant="ghost"
|
|
449
|
+
/>
|
|
450
|
+
</UDropdownMenu>
|
|
451
|
+
</div>
|
|
452
|
+
</template>
|
|
453
|
+
</UTable>
|
|
454
|
+
|
|
455
|
+
<!-- Lazy: the chunk loads on the first open. -->
|
|
456
|
+
<InviteModal
|
|
457
|
+
v-if="inviteMounted"
|
|
458
|
+
v-model:open="inviteOpen"
|
|
459
|
+
:roles="roles"
|
|
460
|
+
:capabilities="capabilities"
|
|
461
|
+
@done="load"
|
|
462
|
+
/>
|
|
463
|
+
<EditRolesSlideover
|
|
464
|
+
v-if="editMounted"
|
|
465
|
+
v-model:open="editOpen"
|
|
466
|
+
:user="editUser"
|
|
467
|
+
:roles="roles"
|
|
468
|
+
:self="editUser?.id === selfId"
|
|
469
|
+
@saved="load"
|
|
470
|
+
/>
|
|
471
|
+
</AdminView>
|
|
472
|
+
</main>
|
|
473
|
+
</template>
|
|
@@ -163,25 +163,33 @@ export default defineNuxtPlugin(() => {
|
|
|
163
163
|
component: SettingsGeneralPanel,
|
|
164
164
|
},
|
|
165
165
|
],
|
|
166
|
-
// The Users
|
|
167
|
-
//
|
|
168
|
-
// has it). Roles is admin
|
|
169
|
-
|
|
166
|
+
// The Users item of the admin menu, between Media (900) and Settings
|
|
167
|
+
// (1000). A click opens the users. Its children are Users and Roles. The
|
|
168
|
+
// item shows for `users:read` (an admin always has it). Roles is admin
|
|
169
|
+
// only: without it, the item is a plain link to the users. The pages are
|
|
170
|
+
// app/pages/admin/users.vue and roles.vue.
|
|
171
|
+
nav: [
|
|
170
172
|
{
|
|
171
173
|
id: 'users',
|
|
172
|
-
order:
|
|
174
|
+
order: 950,
|
|
173
175
|
label: 'supabase.access.users.title',
|
|
174
176
|
icon: 'lucide:users',
|
|
175
|
-
to: '/admin/
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
177
|
+
to: '/admin/users',
|
|
178
|
+
match: ['/admin/roles'],
|
|
179
|
+
enabled: () => usePlutoPermissions().can('users:read'),
|
|
180
|
+
children: [
|
|
181
|
+
{
|
|
182
|
+
label: 'supabase.access.users.title',
|
|
183
|
+
to: '/admin/users',
|
|
184
|
+
icon: 'lucide:users',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
label: 'supabase.access.roles.title',
|
|
188
|
+
to: '/admin/roles',
|
|
189
|
+
icon: 'lucide:shield',
|
|
190
|
+
enabled: () => usePlutoPermissions().isAdmin.value,
|
|
191
|
+
},
|
|
192
|
+
],
|
|
185
193
|
},
|
|
186
194
|
],
|
|
187
195
|
// A guide on the Email tab: the sign-in emails come from Supabase.
|
package/nuxt.config.ts
CHANGED
|
@@ -18,7 +18,9 @@ function supabaseHost(): string {
|
|
|
18
18
|
export default defineNuxtConfig({
|
|
19
19
|
extends: [[coreLayer, { install: true }], utilsLayer],
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// No @nuxt/eslint: a module of a layer runs in every app that extends it. The repo
|
|
22
|
+
// lints with @nuxt/eslint-config (eslint.config.mjs).
|
|
23
|
+
modules: ['@nuxtjs/supabase'],
|
|
22
24
|
|
|
23
25
|
$meta: {
|
|
24
26
|
name: 'supabase',
|
|
@@ -50,15 +52,6 @@ export default defineNuxtConfig({
|
|
|
50
52
|
},
|
|
51
53
|
},
|
|
52
54
|
|
|
53
|
-
eslint: {
|
|
54
|
-
config: {
|
|
55
|
-
nuxt: {
|
|
56
|
-
sortConfigKeys: true,
|
|
57
|
-
},
|
|
58
|
-
standalone: false,
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
|
|
62
55
|
image: {
|
|
63
56
|
domains: supabaseHost() ? [supabaseHost()] : [],
|
|
64
57
|
},
|
package/package.json
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plutocms/supabase",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"trustedDependencies": [
|
|
6
|
-
"@parcel/watcher",
|
|
7
|
-
"@plutocms/core",
|
|
8
|
-
"@plutocms/utils",
|
|
9
|
-
"esbuild",
|
|
10
|
-
"supabase",
|
|
11
|
-
"unrs-resolver",
|
|
12
|
-
"vue-demi"
|
|
13
|
-
],
|
|
4
|
+
"version": "0.19.0",
|
|
14
5
|
"description": "Supabase backend layer for Pluto",
|
|
15
6
|
"author": "Victor Ribeiro",
|
|
16
7
|
"homepage": "https://github.com/plutocms/supabase#readme",
|
|
@@ -27,7 +18,7 @@
|
|
|
27
18
|
"dev": "nuxt dev",
|
|
28
19
|
"generate": "nuxt generate",
|
|
29
20
|
"preview": "nuxt preview",
|
|
30
|
-
"
|
|
21
|
+
"prepare": "nuxt prepare",
|
|
31
22
|
"prettier": "prettier --write .",
|
|
32
23
|
"lint": "eslint .",
|
|
33
24
|
"lint:fix": "eslint . --fix",
|
|
@@ -36,10 +27,9 @@
|
|
|
36
27
|
"supabase-types": "bun ./scripts/supabase-typegen.ts"
|
|
37
28
|
},
|
|
38
29
|
"dependencies": {
|
|
39
|
-
"@nuxt/eslint": "^1.17.0",
|
|
40
30
|
"@nuxt/ui": "^4.12.0",
|
|
41
31
|
"@nuxtjs/supabase": "^2.1.0",
|
|
42
|
-
"@plutocms/core": ">=0.
|
|
32
|
+
"@plutocms/core": ">=0.40.0 <1.0.0",
|
|
43
33
|
"@plutocms/utils": ">=0.3.0 <1.0.0",
|
|
44
34
|
"@supabase/supabase-js": "^2.117.3",
|
|
45
35
|
"postgres": "^3.4.9",
|
|
@@ -48,6 +38,7 @@
|
|
|
48
38
|
},
|
|
49
39
|
"devDependencies": {
|
|
50
40
|
"@antfu/eslint-config": "^9.5.1",
|
|
41
|
+
"@nuxt/eslint-config": "^1.17.0",
|
|
51
42
|
"@types/bun": "^1.4.3",
|
|
52
43
|
"@vueuse/nuxt": "^14.4.0",
|
|
53
44
|
"eslint": "^10.13.0",
|