@plutocms/supabase 0.0.1-alpha.1 → 0.0.1-alpha.11
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/app/components/navbar/NavbarAdmin.vue +5 -5
- package/app/composables/auth.ts +17 -1
- package/app/middleware/auth.ts +5 -1
- package/app/middleware/setup-check.ts +30 -18
- package/app/pages/admin/(auth)/signup.vue +16 -10
- package/app/pages/admin/setup.vue +30 -27
- package/app/pages/admin.vue +1 -0
- package/bun.lock +160 -8
- package/modules/pluto-migrations.ts +71 -0
- package/nuxt.config.ts +13 -19
- package/package.json +6 -4
- package/public/schema.sql +155 -32
- package/server/api/settings/first_setup.get.ts +2 -2
- package/server/api/setup/create.post.ts +44 -10
- package/server/api/signup.post.ts +26 -8
- package/server/plugins/migrations.ts +48 -0
- package/server/utils/migrations.ts +88 -0
- package/server/utils/sql.ts +81 -0
- package/shared/types/supabase.ts +78 -117
|
@@ -24,7 +24,7 @@ const maybeDevUrl = computed(() => {
|
|
|
24
24
|
const items = ref<DropdownMenuItem[][]>([
|
|
25
25
|
[
|
|
26
26
|
{
|
|
27
|
-
label: `${user.value?.
|
|
27
|
+
label: `${user.value?.display_name} (${user.value?.username})` || 'User',
|
|
28
28
|
avatar: {
|
|
29
29
|
icon: 'lucide:user',
|
|
30
30
|
class: 'bg-green-600',
|
|
@@ -53,7 +53,7 @@ const items = ref<DropdownMenuItem[][]>([
|
|
|
53
53
|
|
|
54
54
|
[
|
|
55
55
|
{
|
|
56
|
-
label: 'GitHub',
|
|
56
|
+
label: 'Pluto on GitHub',
|
|
57
57
|
icon: 'i-simple-icons-github',
|
|
58
58
|
type: 'link',
|
|
59
59
|
to: 'https://github.com/ojvribeiro/pluto',
|
|
@@ -135,7 +135,7 @@ defineShortcuts(extractShortcuts(items.value))
|
|
|
135
135
|
<UDropdownMenu
|
|
136
136
|
:items="items"
|
|
137
137
|
:ui="{
|
|
138
|
-
content: 'w-
|
|
138
|
+
content: 'w-64',
|
|
139
139
|
}"
|
|
140
140
|
:modal="false"
|
|
141
141
|
>
|
|
@@ -148,7 +148,7 @@ defineShortcuts(extractShortcuts(items.value))
|
|
|
148
148
|
root: 'rounded-xl',
|
|
149
149
|
icon: 'text-white text-lg',
|
|
150
150
|
}"
|
|
151
|
-
:alt="user?.
|
|
151
|
+
:alt="user?.value?.display_name || 'User Avatar'"
|
|
152
152
|
size="sm"
|
|
153
153
|
icon="lucide:user"
|
|
154
154
|
class="bg-green-600"
|
|
@@ -157,7 +157,7 @@ defineShortcuts(extractShortcuts(items.value))
|
|
|
157
157
|
<span
|
|
158
158
|
class="light:text-zinc-800 text-sm font-bold dark:text-white"
|
|
159
159
|
>
|
|
160
|
-
{{ user?.
|
|
160
|
+
{{ user?.display_name.split(' ')[0] || 'User' }}
|
|
161
161
|
</span>
|
|
162
162
|
</div>
|
|
163
163
|
</button>
|
package/app/composables/auth.ts
CHANGED
|
@@ -23,7 +23,7 @@ export async function useAuth(authOptions?: PlutoSupabaseAuthOptions) {
|
|
|
23
23
|
isSubmitting.value = true
|
|
24
24
|
|
|
25
25
|
try {
|
|
26
|
-
const { error } = await supabase.auth.signInWithPassword({
|
|
26
|
+
const { data, error } = await supabase.auth.signInWithPassword({
|
|
27
27
|
email: form.email,
|
|
28
28
|
password: form.password,
|
|
29
29
|
})
|
|
@@ -36,6 +36,22 @@ export async function useAuth(authOptions?: PlutoSupabaseAuthOptions) {
|
|
|
36
36
|
return
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
if (data.user && data.user.user_metadata.is_admin) {
|
|
40
|
+
// Set the first_setup flag in the healthcheck table to false
|
|
41
|
+
const { error: healthcheckError } = await supabase
|
|
42
|
+
.from('healthcheck')
|
|
43
|
+
.update({ config_value: 'false' })
|
|
44
|
+
.eq('config_name', 'first_setup')
|
|
45
|
+
|
|
46
|
+
if (healthcheckError) {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
message: healthcheckError.message,
|
|
50
|
+
error: healthcheckError,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
return navigateTo(
|
|
40
56
|
(authOptions?.route?.query.redirect as string) ?? '/admin/home'
|
|
41
57
|
)
|
package/app/middleware/auth.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import type { RouteLocationNormalizedGeneric } from 'vue-router'
|
|
2
|
+
|
|
1
3
|
export default defineNuxtRouteMiddleware(async (to, from) => {
|
|
2
|
-
const { isLoggedIn, logout } = await useAuth({
|
|
4
|
+
const { isLoggedIn, logout } = await useAuth({
|
|
5
|
+
route: to as RouteLocationNormalizedGeneric,
|
|
6
|
+
})
|
|
3
7
|
|
|
4
8
|
if (
|
|
5
9
|
isLoggedIn.value &&
|
|
@@ -1,24 +1,36 @@
|
|
|
1
|
-
export default defineNuxtRouteMiddleware(async (to,
|
|
2
|
-
if (to.path
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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') {
|
|
11
20
|
return navigateTo('/admin/setup')
|
|
12
21
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
to.path === '/admin/setup'
|
|
18
|
-
) {
|
|
19
|
-
return navigateTo('/')
|
|
22
|
+
} else {
|
|
23
|
+
// Setup already done — block access to setup page
|
|
24
|
+
if (to.path === '/admin/setup') {
|
|
25
|
+
return navigateTo('/admin/login')
|
|
20
26
|
}
|
|
21
|
-
}
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (import.meta.dev) {
|
|
30
|
+
console.error('Error checking first setup status:', error)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (to.path !== '/admin/setup') {
|
|
22
34
|
return navigateTo('/admin/setup')
|
|
23
35
|
}
|
|
24
36
|
}
|
|
@@ -9,8 +9,8 @@ useHead({
|
|
|
9
9
|
|
|
10
10
|
const form = ref({
|
|
11
11
|
email: '',
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
username: '',
|
|
13
|
+
display_name: '',
|
|
14
14
|
password: '',
|
|
15
15
|
confirm_password: '',
|
|
16
16
|
})
|
|
@@ -32,8 +32,8 @@ async function submitForm() {
|
|
|
32
32
|
method: 'POST',
|
|
33
33
|
body: {
|
|
34
34
|
email: form.value.email,
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
username: form.value.username,
|
|
36
|
+
display_name: form.value.display_name,
|
|
37
37
|
password: form.value.password,
|
|
38
38
|
},
|
|
39
39
|
})
|
|
@@ -60,23 +60,27 @@ async function submitForm() {
|
|
|
60
60
|
type="email"
|
|
61
61
|
placeholder="victor@example.com"
|
|
62
62
|
class="w-full"
|
|
63
|
+
required
|
|
63
64
|
/>
|
|
64
65
|
</UFormField>
|
|
65
66
|
|
|
66
67
|
<div class="flex gap-x-4">
|
|
67
|
-
<UFormField label="
|
|
68
|
+
<UFormField label="Username">
|
|
68
69
|
<UInput
|
|
69
|
-
v-model="form.
|
|
70
|
-
placeholder="e.g:
|
|
70
|
+
v-model="form.username"
|
|
71
|
+
placeholder="e.g: johndoe"
|
|
71
72
|
class="w-full"
|
|
73
|
+
minlength="3"
|
|
74
|
+
required
|
|
72
75
|
/>
|
|
73
76
|
</UFormField>
|
|
74
77
|
|
|
75
|
-
<UFormField label="
|
|
78
|
+
<UFormField label="Display Name">
|
|
76
79
|
<UInput
|
|
77
|
-
v-model="form.
|
|
78
|
-
placeholder="e.g: Doe"
|
|
80
|
+
v-model="form.display_name"
|
|
81
|
+
placeholder="e.g: John Doe"
|
|
79
82
|
class="w-full"
|
|
83
|
+
required
|
|
80
84
|
/>
|
|
81
85
|
</UFormField>
|
|
82
86
|
</div>
|
|
@@ -87,6 +91,7 @@ async function submitForm() {
|
|
|
87
91
|
placeholder="••••"
|
|
88
92
|
class="w-full"
|
|
89
93
|
type="password"
|
|
94
|
+
required
|
|
90
95
|
/>
|
|
91
96
|
</UFormField>
|
|
92
97
|
|
|
@@ -96,6 +101,7 @@ async function submitForm() {
|
|
|
96
101
|
placeholder="••••"
|
|
97
102
|
class="w-full"
|
|
98
103
|
type="password"
|
|
104
|
+
required
|
|
99
105
|
/>
|
|
100
106
|
</UFormField>
|
|
101
107
|
|
|
@@ -23,11 +23,6 @@ const items = ref<StepperItem[]>([
|
|
|
23
23
|
icon: 'i-lucide-database',
|
|
24
24
|
},
|
|
25
25
|
|
|
26
|
-
/* {
|
|
27
|
-
title: 'Admin Account',
|
|
28
|
-
icon: 'i-lucide-user',
|
|
29
|
-
}, */
|
|
30
|
-
|
|
31
26
|
{
|
|
32
27
|
title: 'Complete Setup',
|
|
33
28
|
icon: 'i-lucide-check',
|
|
@@ -61,7 +56,15 @@ const currentStepId = computed(() => {
|
|
|
61
56
|
return undefined
|
|
62
57
|
})
|
|
63
58
|
|
|
64
|
-
|
|
59
|
+
const currentStepSubmitLabel = computed(() => {
|
|
60
|
+
if (currentStep.value === 0) {
|
|
61
|
+
return 'Set up database'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return 'Submit'
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
async function completeDatabaseSetup() {
|
|
65
68
|
try {
|
|
66
69
|
isSettingUpDatabase.value = true
|
|
67
70
|
|
|
@@ -112,8 +115,8 @@ async function completeSetup() {
|
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
function handleStepChange(step: number) {
|
|
115
|
-
if (step ===
|
|
116
|
-
|
|
118
|
+
if (step === 0) {
|
|
119
|
+
completeDatabaseSetup()
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
</script>
|
|
@@ -123,20 +126,14 @@ function handleStepChange(step: number) {
|
|
|
123
126
|
<div class="flex justify-center items-center h-screen">
|
|
124
127
|
<UCard class="w-[500px]">
|
|
125
128
|
<div class="flex flex-col gap-y-8">
|
|
126
|
-
<UStepper
|
|
127
|
-
ref="stepper"
|
|
128
|
-
v-model="currentStep"
|
|
129
|
-
:items="items"
|
|
130
|
-
disabled
|
|
131
|
-
linear
|
|
132
|
-
/>
|
|
129
|
+
<UStepper ref="stepper" v-model="currentStep" :items="items" linear />
|
|
133
130
|
|
|
134
131
|
<div>
|
|
135
132
|
<section v-if="currentStep === 0">
|
|
136
133
|
<UForm
|
|
137
134
|
id="database-setup-form"
|
|
138
135
|
class="flex flex-col gap-y-4"
|
|
139
|
-
@submit="handleStepChange(currentStep
|
|
136
|
+
@submit="handleStepChange(currentStep)"
|
|
140
137
|
>
|
|
141
138
|
<UFormField label="Supabase connection string" required>
|
|
142
139
|
<UInput
|
|
@@ -259,21 +256,28 @@ function handleStepChange(step: number) {
|
|
|
259
256
|
</UForm>
|
|
260
257
|
</section>
|
|
261
258
|
|
|
262
|
-
<section v-
|
|
259
|
+
<section v-if="currentStep === 1">
|
|
263
260
|
<div class="flex flex-col gap-y-4">
|
|
264
|
-
<
|
|
265
|
-
|
|
266
|
-
|
|
261
|
+
<hgroup class="text-center">
|
|
262
|
+
<span class="text-3xl">🎉</span>
|
|
263
|
+
|
|
264
|
+
<h1 class="font-bold text-2xl">Setup Complete</h1>
|
|
265
|
+
</hgroup>
|
|
267
266
|
|
|
268
|
-
<p>
|
|
269
|
-
Your database
|
|
270
|
-
|
|
267
|
+
<p class="text-balance text-center text-lg">
|
|
268
|
+
Your database is ready. Create your admin account by signing
|
|
269
|
+
up.
|
|
271
270
|
</p>
|
|
272
271
|
|
|
273
272
|
<p class="text-center">
|
|
274
|
-
<
|
|
275
|
-
|
|
273
|
+
<UButton
|
|
274
|
+
icon="lucide:arrow-right"
|
|
275
|
+
to="/admin/signup"
|
|
276
|
+
color="primary"
|
|
277
|
+
trailing
|
|
276
278
|
>
|
|
279
|
+
Go to sign up
|
|
280
|
+
</UButton>
|
|
277
281
|
</p>
|
|
278
282
|
</div>
|
|
279
283
|
</section>
|
|
@@ -286,10 +290,9 @@ function handleStepChange(step: number) {
|
|
|
286
290
|
:form="currentStepId"
|
|
287
291
|
:loading="currentLoading"
|
|
288
292
|
leading-icon="lucide:check"
|
|
289
|
-
trailing-icon="lucide:arrow-right"
|
|
290
293
|
type="submit"
|
|
291
294
|
>
|
|
292
|
-
|
|
295
|
+
{{ currentStepSubmitLabel }}
|
|
293
296
|
</UButton>
|
|
294
297
|
</div>
|
|
295
298
|
</div>
|
package/app/pages/admin.vue
CHANGED
|
@@ -14,6 +14,7 @@ watch(visibility, async (current, previous) => {
|
|
|
14
14
|
if (current === 'visible' && previous === 'hidden') {
|
|
15
15
|
if (
|
|
16
16
|
!isLoggedIn.value &&
|
|
17
|
+
!route.path.startsWith('/admin/setup') &&
|
|
17
18
|
route.path.startsWith('/admin/') &&
|
|
18
19
|
route.path !== '/admin/login' &&
|
|
19
20
|
route.path !== '/admin/signup'
|