@plutocms/supabase 0.0.1-alpha.9 → 0.1.1
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 +105 -0
- package/README.md +2 -4
- package/app/assets/css/tailwind.css +5 -0
- package/app/components/navbar/NavbarAdmin.vue +6 -5
- package/app/composables/auth.ts +124 -2
- package/app/middleware/auth.ts +9 -6
- package/app/middleware/setup-check.ts +25 -21
- package/app/pages/admin/(auth)/confirm.vue +38 -0
- package/app/pages/admin/(auth)/forgot-password.vue +88 -0
- package/app/pages/admin/(auth)/login.vue +5 -1
- package/app/pages/admin/(auth)/signup.vue +76 -16
- package/app/pages/admin/(auth)/update-password.vue +134 -0
- package/app/pages/admin/setup.vue +5 -142
- package/app/pages/admin.vue +2 -4
- package/modules/pluto-migrations.ts +121 -0
- package/nuxt.config.ts +14 -1
- package/package.json +40 -24
- package/public/schema.sql +46 -5
- package/scripts/supabase-typegen.ts +36 -21
- package/scripts/tsconfig.json +18 -0
- package/server/api/{signup.post.ts → auth/signup.post.ts} +7 -16
- package/server/api/setup/create.post.ts +39 -82
- package/server/plugins/migrations.ts +60 -0
- package/server/utils/migrations.ts +88 -0
- package/server/utils/sql.ts +81 -0
- package/server/utils/typegen.ts +68 -0
- package/shared/types/runtime-config.d.ts +16 -0
- package/shared/types/supabase.ts +138 -124
- package/.agents/skills/nuxt/GENERATION.md +0 -5
- package/.agents/skills/nuxt/SKILL.md +0 -55
- package/.agents/skills/nuxt/references/advanced-hooks.md +0 -289
- package/.agents/skills/nuxt/references/advanced-layers.md +0 -299
- package/.agents/skills/nuxt/references/advanced-module-authoring.md +0 -554
- package/.agents/skills/nuxt/references/best-practices-data-fetching.md +0 -357
- package/.agents/skills/nuxt/references/best-practices-ssr.md +0 -355
- package/.agents/skills/nuxt/references/core-cli.md +0 -263
- package/.agents/skills/nuxt/references/core-config.md +0 -162
- package/.agents/skills/nuxt/references/core-data-fetching.md +0 -236
- package/.agents/skills/nuxt/references/core-deployment.md +0 -224
- package/.agents/skills/nuxt/references/core-directory-structure.md +0 -269
- package/.agents/skills/nuxt/references/core-modules.md +0 -292
- package/.agents/skills/nuxt/references/core-routing.md +0 -226
- package/.agents/skills/nuxt/references/features-components-autoimport.md +0 -328
- package/.agents/skills/nuxt/references/features-components.md +0 -264
- package/.agents/skills/nuxt/references/features-composables.md +0 -276
- package/.agents/skills/nuxt/references/features-server.md +0 -265
- package/.agents/skills/nuxt/references/features-state.md +0 -194
- package/.agents/skills/nuxt/references/rendering-modes.md +0 -237
- package/.agents/skills/nuxt-ui/SKILL.md +0 -334
- package/.agents/skills/nuxt-ui/references/components.md +0 -377
- package/.agents/skills/nuxt-ui/references/composables.md +0 -127
- package/.agents/skills/nuxt-ui/references/layouts/chat.md +0 -195
- package/.agents/skills/nuxt-ui/references/layouts/dashboard.md +0 -220
- package/.agents/skills/nuxt-ui/references/layouts/docs.md +0 -141
- package/.agents/skills/nuxt-ui/references/layouts/editor.md +0 -167
- package/.agents/skills/nuxt-ui/references/layouts/page.md +0 -260
- package/.agents/skills/nuxt-ui/references/theming.md +0 -427
- package/.agents/skills/supabase-postgres-best-practices/AGENTS.md +0 -68
- package/.agents/skills/supabase-postgres-best-practices/CLAUDE.md +0 -1
- package/.agents/skills/supabase-postgres-best-practices/README.md +0 -116
- package/.agents/skills/supabase-postgres-best-practices/SKILL.md +0 -64
- package/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +0 -55
- package/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +0 -49
- package/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +0 -46
- package/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +0 -44
- package/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +0 -41
- package/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +0 -46
- package/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +0 -54
- package/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +0 -53
- package/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +0 -50
- package/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +0 -50
- package/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +0 -56
- package/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +0 -68
- package/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +0 -50
- package/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +0 -54
- package/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +0 -45
- package/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +0 -55
- package/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +0 -55
- package/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +0 -44
- package/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +0 -40
- package/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +0 -48
- package/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +0 -43
- package/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +0 -45
- package/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +0 -80
- package/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +0 -46
- package/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +0 -59
- package/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +0 -55
- package/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +0 -55
- package/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +0 -61
- package/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +0 -54
- package/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +0 -50
- package/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +0 -57
- package/.editorconfig +0 -12
- package/.eslintignore +0 -1
- package/.prettierrc +0 -9
- package/.vscode/extensions.json +0 -10
- package/.vscode/settings.json +0 -61
- package/bun.lock +0 -2837
- package/eslint.config.mjs +0 -88
- package/server/api/category/list.get.ts +0 -9
- package/server/api/category/new.post.ts +0 -31
- package/skills-lock.json +0 -20
|
@@ -7,10 +7,12 @@ useHead({
|
|
|
7
7
|
title: 'Sign Up',
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
+
const toast = useToast()
|
|
11
|
+
|
|
10
12
|
const form = ref({
|
|
11
13
|
email: '',
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
username: '',
|
|
15
|
+
display_name: '',
|
|
14
16
|
password: '',
|
|
15
17
|
confirm_password: '',
|
|
16
18
|
})
|
|
@@ -21,26 +23,44 @@ const passwordMatch = computed(() => {
|
|
|
21
23
|
return form.value.password === form.value.confirm_password
|
|
22
24
|
})
|
|
23
25
|
|
|
26
|
+
const isEmailVerificationMessageVisible = ref<boolean>(false)
|
|
27
|
+
|
|
24
28
|
async function submitForm() {
|
|
25
29
|
if (!passwordMatch.value) {
|
|
26
30
|
return
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
isSubmitting.value = true
|
|
34
|
+
|
|
30
35
|
try {
|
|
31
|
-
await $fetch('/api/signup', {
|
|
36
|
+
await $fetch('/api/auth/signup', {
|
|
32
37
|
method: 'POST',
|
|
33
38
|
body: {
|
|
34
39
|
email: form.value.email,
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
username: form.value.username,
|
|
41
|
+
display_name: form.value.display_name,
|
|
37
42
|
password: form.value.password,
|
|
38
43
|
},
|
|
39
44
|
})
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
toast.add({
|
|
47
|
+
title: 'Account created successfully',
|
|
48
|
+
description: 'You still need to verify your email before you can log in.',
|
|
49
|
+
icon: 'lucide:check',
|
|
50
|
+
color: 'success',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
isEmailVerificationMessageVisible.value = true
|
|
42
54
|
} catch (error) {
|
|
43
55
|
console.error('Error signing up:', error)
|
|
56
|
+
|
|
57
|
+
toast.add({
|
|
58
|
+
title: 'Could not create account',
|
|
59
|
+
description:
|
|
60
|
+
error instanceof Error ? error.message : 'An unknown error occurred.',
|
|
61
|
+
icon: 'lucide:circle-x',
|
|
62
|
+
color: 'error',
|
|
63
|
+
})
|
|
44
64
|
} finally {
|
|
45
65
|
isSubmitting.value = false
|
|
46
66
|
}
|
|
@@ -50,52 +70,92 @@ async function submitForm() {
|
|
|
50
70
|
<template>
|
|
51
71
|
<div class="grid h-full place-items-center">
|
|
52
72
|
<UCard class="w-100">
|
|
53
|
-
<
|
|
73
|
+
<div v-if="isEmailVerificationMessageVisible" class="space-y-6 py-10">
|
|
74
|
+
<div class="text-center space-y-4">
|
|
75
|
+
<hgroup class="flex flex-col gap-y-1">
|
|
76
|
+
<span class="text-5xl">
|
|
77
|
+
<UIcon name="lucide:mail-warning" class="inline-block" />
|
|
78
|
+
</span>
|
|
79
|
+
|
|
80
|
+
<h1 class="text-3xl font-bold">Almost there!</h1>
|
|
81
|
+
</hgroup>
|
|
82
|
+
|
|
83
|
+
<p>
|
|
84
|
+
A verification link has been sent to your email. Please check your
|
|
85
|
+
inbox and follow the instructions to verify your account.
|
|
86
|
+
</p>
|
|
87
|
+
|
|
88
|
+
<UAlert icon="lucide:check" color="success" variant="outline">
|
|
89
|
+
<template #description>
|
|
90
|
+
You can close this page. You will be logged in automatically once
|
|
91
|
+
you verify your email.
|
|
92
|
+
</template>
|
|
93
|
+
</UAlert>
|
|
94
|
+
|
|
95
|
+
<UAlert
|
|
96
|
+
icon="lucide:alert-triangle"
|
|
97
|
+
color="warning"
|
|
98
|
+
variant="outline"
|
|
99
|
+
>
|
|
100
|
+
<template #description>
|
|
101
|
+
If you don't see the email, please check your spam folder.
|
|
102
|
+
</template>
|
|
103
|
+
</UAlert>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<form v-else @submit.prevent="submitForm">
|
|
54
108
|
<div class="flex flex-col gap-y-6">
|
|
55
109
|
<h1 class="text-3xl font-bold">Create Account</h1>
|
|
56
110
|
|
|
57
|
-
<UFormField label="Email">
|
|
111
|
+
<UFormField label="Email" required>
|
|
58
112
|
<UInput
|
|
59
113
|
v-model="form.email"
|
|
60
114
|
type="email"
|
|
61
115
|
placeholder="victor@example.com"
|
|
62
116
|
class="w-full"
|
|
117
|
+
required
|
|
63
118
|
/>
|
|
64
119
|
</UFormField>
|
|
65
120
|
|
|
66
121
|
<div class="flex gap-x-4">
|
|
67
|
-
<UFormField label="
|
|
122
|
+
<UFormField label="Username" required>
|
|
68
123
|
<UInput
|
|
69
|
-
v-model="form.
|
|
70
|
-
placeholder="e.g:
|
|
124
|
+
v-model="form.username"
|
|
125
|
+
placeholder="e.g: johndoe"
|
|
71
126
|
class="w-full"
|
|
127
|
+
minlength="3"
|
|
128
|
+
required
|
|
72
129
|
/>
|
|
73
130
|
</UFormField>
|
|
74
131
|
|
|
75
|
-
<UFormField label="
|
|
132
|
+
<UFormField label="Display Name" required>
|
|
76
133
|
<UInput
|
|
77
|
-
v-model="form.
|
|
78
|
-
placeholder="e.g: Doe"
|
|
134
|
+
v-model="form.display_name"
|
|
135
|
+
placeholder="e.g: John Doe"
|
|
79
136
|
class="w-full"
|
|
137
|
+
required
|
|
80
138
|
/>
|
|
81
139
|
</UFormField>
|
|
82
140
|
</div>
|
|
83
141
|
|
|
84
|
-
<UFormField :error="!passwordMatch" label="Password">
|
|
142
|
+
<UFormField :error="!passwordMatch" label="Password" required>
|
|
85
143
|
<UInput
|
|
86
144
|
v-model="form.password"
|
|
87
145
|
placeholder="••••"
|
|
88
146
|
class="w-full"
|
|
89
147
|
type="password"
|
|
148
|
+
required
|
|
90
149
|
/>
|
|
91
150
|
</UFormField>
|
|
92
151
|
|
|
93
|
-
<UFormField :error="!passwordMatch" label="Confirm Password">
|
|
152
|
+
<UFormField :error="!passwordMatch" label="Confirm Password" required>
|
|
94
153
|
<UInput
|
|
95
154
|
v-model="form.confirm_password"
|
|
96
155
|
placeholder="••••"
|
|
97
156
|
class="w-full"
|
|
98
157
|
type="password"
|
|
158
|
+
required
|
|
99
159
|
/>
|
|
100
160
|
</UFormField>
|
|
101
161
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({
|
|
3
|
+
layout: 'auth',
|
|
4
|
+
})
|
|
5
|
+
|
|
6
|
+
useHead({
|
|
7
|
+
title: 'Update Password',
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const route = useRoute()
|
|
11
|
+
const supabase = useSupabaseClient()
|
|
12
|
+
|
|
13
|
+
// Exchange the PKCE code for a session when arriving from a password reset email
|
|
14
|
+
onMounted(async () => {
|
|
15
|
+
const code = route.query.code as string | undefined
|
|
16
|
+
|
|
17
|
+
if (code) {
|
|
18
|
+
await supabase.auth.exchangeCodeForSession(code)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const { isSubmitting, updatePassword } = await useAuth()
|
|
23
|
+
|
|
24
|
+
const form = ref({
|
|
25
|
+
password: '',
|
|
26
|
+
confirm_password: '',
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const passwordMatch = computed(() => {
|
|
30
|
+
return form.value.password === form.value.confirm_password
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const isPasswordUpdated = ref(false)
|
|
34
|
+
|
|
35
|
+
async function submitForm() {
|
|
36
|
+
if (!passwordMatch.value) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = await updatePassword(form.value.password)
|
|
41
|
+
|
|
42
|
+
if (result.success) {
|
|
43
|
+
isPasswordUpdated.value = true
|
|
44
|
+
|
|
45
|
+
await supabase.auth.signOut()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<div class="grid h-full place-items-center">
|
|
52
|
+
<UCard class="w-100">
|
|
53
|
+
<div v-if="isPasswordUpdated" class="space-y-6 py-10">
|
|
54
|
+
<div class="text-center space-y-4">
|
|
55
|
+
<hgroup class="flex flex-col gap-y-1">
|
|
56
|
+
<span class="text-5xl">
|
|
57
|
+
<UIcon name="lucide:lock-keyhole" class="inline-block" />
|
|
58
|
+
</span>
|
|
59
|
+
|
|
60
|
+
<h1 class="text-3xl font-bold">Password Updated!</h1>
|
|
61
|
+
</hgroup>
|
|
62
|
+
|
|
63
|
+
<p>
|
|
64
|
+
Your password has been changed. You can now log in with your new
|
|
65
|
+
password.
|
|
66
|
+
</p>
|
|
67
|
+
|
|
68
|
+
<div class="flex items-center justify-center">
|
|
69
|
+
<UButton to="/admin/login" color="primary" icon="lucide:log-in">
|
|
70
|
+
Go to Login
|
|
71
|
+
</UButton>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<form v-else @submit.prevent="submitForm">
|
|
77
|
+
<div class="flex flex-col gap-y-6">
|
|
78
|
+
<hgroup class="flex flex-col gap-y-1">
|
|
79
|
+
<h1 class="text-3xl font-bold">Update Password</h1>
|
|
80
|
+
<p class="text-sm text-muted">Enter your new password below.</p>
|
|
81
|
+
</hgroup>
|
|
82
|
+
|
|
83
|
+
<UFormField label="New Password">
|
|
84
|
+
<UInput
|
|
85
|
+
v-model="form.password"
|
|
86
|
+
placeholder="••••"
|
|
87
|
+
type="password"
|
|
88
|
+
class="w-full"
|
|
89
|
+
autocomplete="new-password"
|
|
90
|
+
required
|
|
91
|
+
/>
|
|
92
|
+
</UFormField>
|
|
93
|
+
|
|
94
|
+
<UFormField
|
|
95
|
+
:error="
|
|
96
|
+
form.confirm_password && !passwordMatch
|
|
97
|
+
? 'Passwords do not match.'
|
|
98
|
+
: undefined
|
|
99
|
+
"
|
|
100
|
+
label="Confirm New Password"
|
|
101
|
+
>
|
|
102
|
+
<UInput
|
|
103
|
+
v-model="form.confirm_password"
|
|
104
|
+
placeholder="••••"
|
|
105
|
+
type="password"
|
|
106
|
+
class="w-full"
|
|
107
|
+
autocomplete="new-password"
|
|
108
|
+
required
|
|
109
|
+
/>
|
|
110
|
+
</UFormField>
|
|
111
|
+
|
|
112
|
+
<div class="flex items-center justify-between gap-x-4">
|
|
113
|
+
<UFormField>
|
|
114
|
+
<UButton
|
|
115
|
+
:loading="isSubmitting"
|
|
116
|
+
:disabled="!passwordMatch"
|
|
117
|
+
type="submit"
|
|
118
|
+
icon="lucide:lock-keyhole"
|
|
119
|
+
>
|
|
120
|
+
Update Password
|
|
121
|
+
</UButton>
|
|
122
|
+
</UFormField>
|
|
123
|
+
|
|
124
|
+
<UFormField>
|
|
125
|
+
<UButton variant="link" to="/admin/login" class="px-0">
|
|
126
|
+
Back to Login
|
|
127
|
+
</UButton>
|
|
128
|
+
</UFormField>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</form>
|
|
132
|
+
</UCard>
|
|
133
|
+
</div>
|
|
134
|
+
</template>
|
|
@@ -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',
|
|
@@ -39,29 +34,17 @@ const stepper = useTemplateRef('stepper')
|
|
|
39
34
|
const currentStep = ref<number>(0)
|
|
40
35
|
|
|
41
36
|
const isSettingUpDatabase = ref(false)
|
|
42
|
-
const isCreatingAdmin = ref(false)
|
|
43
37
|
|
|
44
38
|
const databaseForm = ref({
|
|
45
39
|
connectionString: '',
|
|
46
40
|
password: '',
|
|
47
41
|
})
|
|
48
42
|
|
|
49
|
-
const adminForm = ref({
|
|
50
|
-
username: '',
|
|
51
|
-
display_name: '',
|
|
52
|
-
email: '',
|
|
53
|
-
password: '',
|
|
54
|
-
})
|
|
55
|
-
|
|
56
43
|
const currentLoading = computed(() => {
|
|
57
44
|
if (currentStep.value === 0) {
|
|
58
45
|
return isSettingUpDatabase.value
|
|
59
46
|
}
|
|
60
47
|
|
|
61
|
-
if (currentStep.value === 1) {
|
|
62
|
-
return isCreatingAdmin.value
|
|
63
|
-
}
|
|
64
|
-
|
|
65
48
|
return false
|
|
66
49
|
})
|
|
67
50
|
|
|
@@ -70,10 +53,6 @@ const currentStepId = computed(() => {
|
|
|
70
53
|
return 'database-setup-form'
|
|
71
54
|
}
|
|
72
55
|
|
|
73
|
-
if (currentStep.value === 1) {
|
|
74
|
-
return 'admin-setup-form'
|
|
75
|
-
}
|
|
76
|
-
|
|
77
56
|
return undefined
|
|
78
57
|
})
|
|
79
58
|
|
|
@@ -82,10 +61,6 @@ const currentStepSubmitLabel = computed(() => {
|
|
|
82
61
|
return 'Set up database'
|
|
83
62
|
}
|
|
84
63
|
|
|
85
|
-
if (currentStep.value === 1) {
|
|
86
|
-
return 'Create admin account'
|
|
87
|
-
}
|
|
88
|
-
|
|
89
64
|
return 'Submit'
|
|
90
65
|
})
|
|
91
66
|
|
|
@@ -101,7 +76,6 @@ async function completeDatabaseSetup() {
|
|
|
101
76
|
'[YOUR-PASSWORD]',
|
|
102
77
|
encodeURIComponent(databaseForm.value.password)
|
|
103
78
|
),
|
|
104
|
-
adminForm: adminForm.value,
|
|
105
79
|
},
|
|
106
80
|
})
|
|
107
81
|
|
|
@@ -140,71 +114,17 @@ async function completeDatabaseSetup() {
|
|
|
140
114
|
}
|
|
141
115
|
}
|
|
142
116
|
|
|
143
|
-
async function createAdminAccount() {
|
|
144
|
-
try {
|
|
145
|
-
isCreatingAdmin.value = true
|
|
146
|
-
|
|
147
|
-
const data = await $fetch('/api/signup', {
|
|
148
|
-
method: 'POST',
|
|
149
|
-
body: {
|
|
150
|
-
username: adminForm.value.username,
|
|
151
|
-
display_name: adminForm.value.display_name,
|
|
152
|
-
email: adminForm.value.email,
|
|
153
|
-
password: adminForm.value.password,
|
|
154
|
-
is_admin: true,
|
|
155
|
-
},
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
if (data.success === false) {
|
|
159
|
-
toast.add({
|
|
160
|
-
title: 'Admin account creation failed',
|
|
161
|
-
description: data.message || 'Please try again.',
|
|
162
|
-
icon: 'lucide:circle-x',
|
|
163
|
-
color: 'error',
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
return
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
toast.add({
|
|
170
|
-
title: 'Admin account created',
|
|
171
|
-
description: 'Your admin account has been created successfully.',
|
|
172
|
-
icon: 'lucide:check-circle',
|
|
173
|
-
color: 'success',
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
stepper.value?.next()
|
|
177
|
-
} catch (error) {
|
|
178
|
-
if (import.meta.dev) {
|
|
179
|
-
console.error('Error creating admin account:', error)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
toast.add({
|
|
183
|
-
title: 'Error creating admin account',
|
|
184
|
-
description: 'Please check the console for more details.',
|
|
185
|
-
icon: 'lucide:circle-x',
|
|
186
|
-
color: 'error',
|
|
187
|
-
})
|
|
188
|
-
} finally {
|
|
189
|
-
isCreatingAdmin.value = false
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
117
|
function handleStepChange(step: number) {
|
|
194
118
|
if (step === 0) {
|
|
195
119
|
completeDatabaseSetup()
|
|
196
120
|
}
|
|
197
|
-
|
|
198
|
-
if (step === 1) {
|
|
199
|
-
createAdminAccount()
|
|
200
|
-
}
|
|
201
121
|
}
|
|
202
122
|
</script>
|
|
203
123
|
|
|
204
124
|
<template>
|
|
205
125
|
<UApp>
|
|
206
126
|
<div class="flex justify-center items-center h-screen">
|
|
207
|
-
<UCard class="w-
|
|
127
|
+
<UCard class="w-125">
|
|
208
128
|
<div class="flex flex-col gap-y-8">
|
|
209
129
|
<UStepper ref="stepper" v-model="currentStep" :items="items" linear />
|
|
210
130
|
|
|
@@ -337,52 +257,6 @@ function handleStepChange(step: number) {
|
|
|
337
257
|
</section>
|
|
338
258
|
|
|
339
259
|
<section v-if="currentStep === 1">
|
|
340
|
-
<UForm
|
|
341
|
-
id="admin-setup-form"
|
|
342
|
-
class="grid grid-cols-2 gap-4"
|
|
343
|
-
@submit="handleStepChange(currentStep)"
|
|
344
|
-
>
|
|
345
|
-
<UFormField label="Email" class="col-span-2" required>
|
|
346
|
-
<UInput
|
|
347
|
-
v-model="adminForm.email"
|
|
348
|
-
:disabled="isCreatingAdmin"
|
|
349
|
-
placeholder="you@example.com"
|
|
350
|
-
autofocus
|
|
351
|
-
required
|
|
352
|
-
/>
|
|
353
|
-
</UFormField>
|
|
354
|
-
|
|
355
|
-
<UFormField label="Username" required>
|
|
356
|
-
<UInput
|
|
357
|
-
v-model="adminForm.username"
|
|
358
|
-
:disabled="isCreatingAdmin"
|
|
359
|
-
placeholder="Username"
|
|
360
|
-
required
|
|
361
|
-
/>
|
|
362
|
-
</UFormField>
|
|
363
|
-
|
|
364
|
-
<UFormField label="Display Name" required>
|
|
365
|
-
<UInput
|
|
366
|
-
v-model="adminForm.display_name"
|
|
367
|
-
:disabled="isCreatingAdmin"
|
|
368
|
-
placeholder="Display Name"
|
|
369
|
-
required
|
|
370
|
-
/>
|
|
371
|
-
</UFormField>
|
|
372
|
-
|
|
373
|
-
<UFormField label="Password" class="col-span-2" required>
|
|
374
|
-
<UInput
|
|
375
|
-
v-model="adminForm.password"
|
|
376
|
-
:disabled="isCreatingAdmin"
|
|
377
|
-
type="password"
|
|
378
|
-
placeholder="• • • • "
|
|
379
|
-
required
|
|
380
|
-
/>
|
|
381
|
-
</UFormField>
|
|
382
|
-
</UForm>
|
|
383
|
-
</section>
|
|
384
|
-
|
|
385
|
-
<section v-else-if="currentStep === 2">
|
|
386
260
|
<div class="flex flex-col gap-y-4">
|
|
387
261
|
<hgroup class="text-center">
|
|
388
262
|
<span class="text-3xl">🎉</span>
|
|
@@ -391,29 +265,18 @@ function handleStepChange(step: number) {
|
|
|
391
265
|
</hgroup>
|
|
392
266
|
|
|
393
267
|
<p class="text-balance text-center text-lg">
|
|
394
|
-
Your database is ready
|
|
395
|
-
|
|
268
|
+
Your database is ready. Create your admin account by signing
|
|
269
|
+
up.
|
|
396
270
|
</p>
|
|
397
271
|
|
|
398
|
-
<UAlert icon="lucide:info" color="warning" variant="outline">
|
|
399
|
-
<template #description>
|
|
400
|
-
<p>
|
|
401
|
-
You need to confirm your email address before you can log
|
|
402
|
-
in. Please check your inbox and click on the confirmation
|
|
403
|
-
link. If you don't see the email, please check your spam
|
|
404
|
-
folder.
|
|
405
|
-
</p>
|
|
406
|
-
</template>
|
|
407
|
-
</UAlert>
|
|
408
|
-
|
|
409
272
|
<p class="text-center">
|
|
410
273
|
<UButton
|
|
411
274
|
icon="lucide:arrow-right"
|
|
412
|
-
to="/admin/
|
|
275
|
+
to="/admin/signup"
|
|
413
276
|
color="primary"
|
|
414
277
|
trailing
|
|
415
278
|
>
|
|
416
|
-
Go to
|
|
279
|
+
Go to sign up
|
|
417
280
|
</UButton>
|
|
418
281
|
</p>
|
|
419
282
|
</div>
|
package/app/pages/admin.vue
CHANGED
|
@@ -5,7 +5,7 @@ definePageMeta({
|
|
|
5
5
|
})
|
|
6
6
|
|
|
7
7
|
const route = useRoute()
|
|
8
|
-
const { isLoggedIn, logout } = await useAuth()
|
|
8
|
+
const { isLoggedIn, logout, allowedUnauthenticatedPaths } = await useAuth()
|
|
9
9
|
const toast = useToast()
|
|
10
10
|
|
|
11
11
|
const visibility = useDocumentVisibility()
|
|
@@ -15,9 +15,7 @@ watch(visibility, async (current, previous) => {
|
|
|
15
15
|
if (
|
|
16
16
|
!isLoggedIn.value &&
|
|
17
17
|
!route.path.startsWith('/admin/setup') &&
|
|
18
|
-
route.path
|
|
19
|
-
route.path !== '/admin/login' &&
|
|
20
|
-
route.path !== '/admin/signup'
|
|
18
|
+
!allowedUnauthenticatedPaths.includes(route.path)
|
|
21
19
|
) {
|
|
22
20
|
await logout({ redirectTo: route.path })
|
|
23
21
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, relative } from 'node:path'
|
|
3
|
+
import { createResolver, defineNuxtModule, getLayerDirectories } from 'nuxt/kit'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Discovers schema.[layerName].sql files across all Nuxt layers,
|
|
7
|
+
* reads their content at build time, and populates runtimeConfig
|
|
8
|
+
* so the migrations plugin can run them at server startup.
|
|
9
|
+
*
|
|
10
|
+
* Also seeds the consuming app's shared/types/supabase.ts with a
|
|
11
|
+
* re-export of this layer's own committed snapshot when the consumer
|
|
12
|
+
* has none yet, so `Database` resolves to real types instead of
|
|
13
|
+
* `unknown` on first run. A re-export (rather than a full copy) keeps
|
|
14
|
+
* a single canonical `Database` declaration so Nuxt's shared-imports
|
|
15
|
+
* scanning across layers doesn't see two conflicting definitions. The
|
|
16
|
+
* runtime migration plugin overwrites this with a fully generated file
|
|
17
|
+
* once real migrations run (see server/plugins/migrations.ts).
|
|
18
|
+
*/
|
|
19
|
+
export default defineNuxtModule({
|
|
20
|
+
meta: {
|
|
21
|
+
name: 'pluto-migrations',
|
|
22
|
+
configKey: 'plutoMigrations',
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
setup(_options, nuxt) {
|
|
26
|
+
// Let the runtime migration plugin know where the consumer's
|
|
27
|
+
// project root is, so it can write the generated types file there.
|
|
28
|
+
nuxt.options.runtimeConfig.plutoRootDir = nuxt.options.rootDir
|
|
29
|
+
|
|
30
|
+
const consumerTypesPath = join(
|
|
31
|
+
nuxt.options.rootDir,
|
|
32
|
+
'shared/types/supabase.ts'
|
|
33
|
+
)
|
|
34
|
+
if (!existsSync(consumerTypesPath)) {
|
|
35
|
+
// Resolve *this exact* @plutocms/supabase instance's own snapshot
|
|
36
|
+
// (local-linked or npm-installed, whichever is actually running)
|
|
37
|
+
// rather than a bare package specifier, which would re-resolve
|
|
38
|
+
// through the consumer's own node_modules and could pick up a
|
|
39
|
+
// different, possibly stale, copy of the package.
|
|
40
|
+
const { resolve } = createResolver(import.meta.url)
|
|
41
|
+
const ownTypesPath = resolve('../shared/types/supabase.ts')
|
|
42
|
+
const consumerTypesDir = dirname(consumerTypesPath)
|
|
43
|
+
let importSpecifier = relative(consumerTypesDir, ownTypesPath)
|
|
44
|
+
.replace(/\\/g, '/')
|
|
45
|
+
.replace(/\.ts$/, '')
|
|
46
|
+
if (!importSpecifier.startsWith('.')) {
|
|
47
|
+
importSpecifier = `./${importSpecifier}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
mkdirSync(consumerTypesDir, { recursive: true })
|
|
51
|
+
writeFileSync(
|
|
52
|
+
consumerTypesPath,
|
|
53
|
+
`export * from '${importSpecifier}'\n`,
|
|
54
|
+
'utf-8'
|
|
55
|
+
)
|
|
56
|
+
console.warn(
|
|
57
|
+
'[pluto-migrations] Seeded shared/types/supabase.ts (re-exporting @plutocms/supabase types; no generated types found yet).'
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const layerDirs = getLayerDirectories()
|
|
62
|
+
const layerSchemas: Record<string, string> = {}
|
|
63
|
+
const schemaPattern = /^schema\.(.+)\.sql$/
|
|
64
|
+
|
|
65
|
+
for (const layer of layerDirs) {
|
|
66
|
+
// Each layer may have a public/ directory with schema files
|
|
67
|
+
const publicDir = join(layer.root, 'public')
|
|
68
|
+
|
|
69
|
+
if (!existsSync(publicDir)) {
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let files: string[] = []
|
|
74
|
+
try {
|
|
75
|
+
files = readdirSync(publicDir, 'utf-8')
|
|
76
|
+
} catch {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
// Skip the core schema
|
|
82
|
+
if (file === 'schema.sql') {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const match = file.match(schemaPattern)
|
|
87
|
+
if (match?.[1]) {
|
|
88
|
+
const layerName = match[1]
|
|
89
|
+
// Read SQL content at build time
|
|
90
|
+
try {
|
|
91
|
+
layerSchemas[layerName] = readFileSync(
|
|
92
|
+
join(publicDir, file),
|
|
93
|
+
'utf-8'
|
|
94
|
+
)
|
|
95
|
+
} catch {
|
|
96
|
+
console.error(
|
|
97
|
+
`[pluto-migrations] Failed to read schema file: ${file}`
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const layerNames = Object.keys(layerSchemas)
|
|
105
|
+
|
|
106
|
+
// Populate runtimeConfig with layer names and their SQL content.
|
|
107
|
+
// Nuxt's schema inference narrows this to a literal shape based on
|
|
108
|
+
// whatever layer keys were last observed in this project (see the
|
|
109
|
+
// `RuntimeConfig` augmentation in shared/types/runtime-config.d.ts,
|
|
110
|
+
// which isn't visible from this module-context tsconfig), so the
|
|
111
|
+
// assignment is cast to the intended general shape.
|
|
112
|
+
;(nuxt.options.runtimeConfig as { plutoLayerSchemas: Record<string, string> }).plutoLayerSchemas =
|
|
113
|
+
layerSchemas
|
|
114
|
+
|
|
115
|
+
if (layerNames.length > 0) {
|
|
116
|
+
console.warn(
|
|
117
|
+
`[pluto-migrations] Discovered layer schemas: ${layerNames.join(', ')}`
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
})
|