@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.
Files changed (102) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +2 -4
  3. package/app/assets/css/tailwind.css +5 -0
  4. package/app/components/navbar/NavbarAdmin.vue +6 -5
  5. package/app/composables/auth.ts +124 -2
  6. package/app/middleware/auth.ts +9 -6
  7. package/app/middleware/setup-check.ts +25 -21
  8. package/app/pages/admin/(auth)/confirm.vue +38 -0
  9. package/app/pages/admin/(auth)/forgot-password.vue +88 -0
  10. package/app/pages/admin/(auth)/login.vue +5 -1
  11. package/app/pages/admin/(auth)/signup.vue +76 -16
  12. package/app/pages/admin/(auth)/update-password.vue +134 -0
  13. package/app/pages/admin/setup.vue +5 -142
  14. package/app/pages/admin.vue +2 -4
  15. package/modules/pluto-migrations.ts +121 -0
  16. package/nuxt.config.ts +14 -1
  17. package/package.json +40 -24
  18. package/public/schema.sql +46 -5
  19. package/scripts/supabase-typegen.ts +36 -21
  20. package/scripts/tsconfig.json +18 -0
  21. package/server/api/{signup.post.ts → auth/signup.post.ts} +7 -16
  22. package/server/api/setup/create.post.ts +39 -82
  23. package/server/plugins/migrations.ts +60 -0
  24. package/server/utils/migrations.ts +88 -0
  25. package/server/utils/sql.ts +81 -0
  26. package/server/utils/typegen.ts +68 -0
  27. package/shared/types/runtime-config.d.ts +16 -0
  28. package/shared/types/supabase.ts +138 -124
  29. package/.agents/skills/nuxt/GENERATION.md +0 -5
  30. package/.agents/skills/nuxt/SKILL.md +0 -55
  31. package/.agents/skills/nuxt/references/advanced-hooks.md +0 -289
  32. package/.agents/skills/nuxt/references/advanced-layers.md +0 -299
  33. package/.agents/skills/nuxt/references/advanced-module-authoring.md +0 -554
  34. package/.agents/skills/nuxt/references/best-practices-data-fetching.md +0 -357
  35. package/.agents/skills/nuxt/references/best-practices-ssr.md +0 -355
  36. package/.agents/skills/nuxt/references/core-cli.md +0 -263
  37. package/.agents/skills/nuxt/references/core-config.md +0 -162
  38. package/.agents/skills/nuxt/references/core-data-fetching.md +0 -236
  39. package/.agents/skills/nuxt/references/core-deployment.md +0 -224
  40. package/.agents/skills/nuxt/references/core-directory-structure.md +0 -269
  41. package/.agents/skills/nuxt/references/core-modules.md +0 -292
  42. package/.agents/skills/nuxt/references/core-routing.md +0 -226
  43. package/.agents/skills/nuxt/references/features-components-autoimport.md +0 -328
  44. package/.agents/skills/nuxt/references/features-components.md +0 -264
  45. package/.agents/skills/nuxt/references/features-composables.md +0 -276
  46. package/.agents/skills/nuxt/references/features-server.md +0 -265
  47. package/.agents/skills/nuxt/references/features-state.md +0 -194
  48. package/.agents/skills/nuxt/references/rendering-modes.md +0 -237
  49. package/.agents/skills/nuxt-ui/SKILL.md +0 -334
  50. package/.agents/skills/nuxt-ui/references/components.md +0 -377
  51. package/.agents/skills/nuxt-ui/references/composables.md +0 -127
  52. package/.agents/skills/nuxt-ui/references/layouts/chat.md +0 -195
  53. package/.agents/skills/nuxt-ui/references/layouts/dashboard.md +0 -220
  54. package/.agents/skills/nuxt-ui/references/layouts/docs.md +0 -141
  55. package/.agents/skills/nuxt-ui/references/layouts/editor.md +0 -167
  56. package/.agents/skills/nuxt-ui/references/layouts/page.md +0 -260
  57. package/.agents/skills/nuxt-ui/references/theming.md +0 -427
  58. package/.agents/skills/supabase-postgres-best-practices/AGENTS.md +0 -68
  59. package/.agents/skills/supabase-postgres-best-practices/CLAUDE.md +0 -1
  60. package/.agents/skills/supabase-postgres-best-practices/README.md +0 -116
  61. package/.agents/skills/supabase-postgres-best-practices/SKILL.md +0 -64
  62. package/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +0 -55
  63. package/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +0 -49
  64. package/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +0 -46
  65. package/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +0 -44
  66. package/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +0 -41
  67. package/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +0 -46
  68. package/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +0 -54
  69. package/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +0 -53
  70. package/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +0 -50
  71. package/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +0 -50
  72. package/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +0 -56
  73. package/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +0 -68
  74. package/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +0 -50
  75. package/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +0 -54
  76. package/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +0 -45
  77. package/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +0 -55
  78. package/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +0 -55
  79. package/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +0 -44
  80. package/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +0 -40
  81. package/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +0 -48
  82. package/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +0 -43
  83. package/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +0 -45
  84. package/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +0 -80
  85. package/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +0 -46
  86. package/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +0 -59
  87. package/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +0 -55
  88. package/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +0 -55
  89. package/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +0 -61
  90. package/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +0 -54
  91. package/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +0 -50
  92. package/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +0 -57
  93. package/.editorconfig +0 -12
  94. package/.eslintignore +0 -1
  95. package/.prettierrc +0 -9
  96. package/.vscode/extensions.json +0 -10
  97. package/.vscode/settings.json +0 -61
  98. package/bun.lock +0 -2837
  99. package/eslint.config.mjs +0 -88
  100. package/server/api/category/list.get.ts +0 -9
  101. package/server/api/category/new.post.ts +0 -31
  102. 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
- first_name: '',
13
- last_name: '',
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
- first_name: form.value.first_name,
36
- last_name: form.value.last_name,
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
- navigateTo('/admin/login?rel=new')
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
- <form @submit.prevent="submitForm">
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="First Name">
122
+ <UFormField label="Username" required>
68
123
  <UInput
69
- v-model="form.first_name"
70
- placeholder="e.g: John"
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="Last Name">
132
+ <UFormField label="Display Name" required>
76
133
  <UInput
77
- v-model="form.last_name"
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-[500px]">
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 and your admin account has been
395
- created.
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/login"
275
+ to="/admin/signup"
413
276
  color="primary"
414
277
  trailing
415
278
  >
416
- Go to login page
279
+ Go to sign up
417
280
  </UButton>
418
281
  </p>
419
282
  </div>
@@ -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.startsWith('/admin/') &&
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
+ })