@plutocms/supabase 0.0.1-alpha.7 → 0.0.1-alpha.9

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.
@@ -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({ route: to })
4
+ const { isLoggedIn, logout } = await useAuth({
5
+ route: to as RouteLocationNormalizedGeneric,
6
+ })
3
7
 
4
8
  if (
5
9
  isLoggedIn.value &&
@@ -23,10 +23,10 @@ const items = ref<StepperItem[]>([
23
23
  icon: 'i-lucide-database',
24
24
  },
25
25
 
26
- /* {
26
+ {
27
27
  title: 'Admin Account',
28
28
  icon: 'i-lucide-user',
29
- }, */
29
+ },
30
30
 
31
31
  {
32
32
  title: 'Complete Setup',
@@ -39,17 +39,29 @@ const stepper = useTemplateRef('stepper')
39
39
  const currentStep = ref<number>(0)
40
40
 
41
41
  const isSettingUpDatabase = ref(false)
42
+ const isCreatingAdmin = ref(false)
42
43
 
43
44
  const databaseForm = ref({
44
45
  connectionString: '',
45
46
  password: '',
46
47
  })
47
48
 
49
+ const adminForm = ref({
50
+ username: '',
51
+ display_name: '',
52
+ email: '',
53
+ password: '',
54
+ })
55
+
48
56
  const currentLoading = computed(() => {
49
57
  if (currentStep.value === 0) {
50
58
  return isSettingUpDatabase.value
51
59
  }
52
60
 
61
+ if (currentStep.value === 1) {
62
+ return isCreatingAdmin.value
63
+ }
64
+
53
65
  return false
54
66
  })
55
67
 
@@ -58,10 +70,26 @@ const currentStepId = computed(() => {
58
70
  return 'database-setup-form'
59
71
  }
60
72
 
73
+ if (currentStep.value === 1) {
74
+ return 'admin-setup-form'
75
+ }
76
+
61
77
  return undefined
62
78
  })
63
79
 
64
- async function completeSetup() {
80
+ const currentStepSubmitLabel = computed(() => {
81
+ if (currentStep.value === 0) {
82
+ return 'Set up database'
83
+ }
84
+
85
+ if (currentStep.value === 1) {
86
+ return 'Create admin account'
87
+ }
88
+
89
+ return 'Submit'
90
+ })
91
+
92
+ async function completeDatabaseSetup() {
65
93
  try {
66
94
  isSettingUpDatabase.value = true
67
95
 
@@ -73,6 +101,7 @@ async function completeSetup() {
73
101
  '[YOUR-PASSWORD]',
74
102
  encodeURIComponent(databaseForm.value.password)
75
103
  ),
104
+ adminForm: adminForm.value,
76
105
  },
77
106
  })
78
107
 
@@ -111,9 +140,63 @@ async function completeSetup() {
111
140
  }
112
141
  }
113
142
 
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
+
114
193
  function handleStepChange(step: number) {
194
+ if (step === 0) {
195
+ completeDatabaseSetup()
196
+ }
197
+
115
198
  if (step === 1) {
116
- completeSetup()
199
+ createAdminAccount()
117
200
  }
118
201
  }
119
202
  </script>
@@ -123,20 +206,14 @@ function handleStepChange(step: number) {
123
206
  <div class="flex justify-center items-center h-screen">
124
207
  <UCard class="w-[500px]">
125
208
  <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
- />
209
+ <UStepper ref="stepper" v-model="currentStep" :items="items" linear />
133
210
 
134
211
  <div>
135
212
  <section v-if="currentStep === 0">
136
213
  <UForm
137
214
  id="database-setup-form"
138
215
  class="flex flex-col gap-y-4"
139
- @submit="handleStepChange(currentStep + 1)"
216
+ @submit="handleStepChange(currentStep)"
140
217
  >
141
218
  <UFormField label="Supabase connection string" required>
142
219
  <UInput
@@ -259,21 +336,85 @@ function handleStepChange(step: number) {
259
336
  </UForm>
260
337
  </section>
261
338
 
262
- <section v-else-if="currentStep === 1">
339
+ <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">
263
386
  <div class="flex flex-col gap-y-4">
264
- <h1 class="text-2xl font-bold text-center">
265
- Setup Complete 🎉
266
- </h1>
387
+ <hgroup class="text-center">
388
+ <span class="text-3xl">🎉</span>
267
389
 
268
- <p>
269
- Your database has been set up successfully! 🎉 You can now log
270
- in to the admin panel using your Supabase credentials.
390
+ <h1 class="font-bold text-2xl">Setup Complete</h1>
391
+ </hgroup>
392
+
393
+ <p class="text-balance text-center text-lg">
394
+ Your database is ready and your admin account has been
395
+ created.
271
396
  </p>
272
397
 
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
+
273
409
  <p class="text-center">
274
- <ULink to="/admin/login" class="underline">
275
- Go to login page</ULink
410
+ <UButton
411
+ icon="lucide:arrow-right"
412
+ to="/admin/login"
413
+ color="primary"
414
+ trailing
276
415
  >
416
+ Go to login page
417
+ </UButton>
277
418
  </p>
278
419
  </div>
279
420
  </section>
@@ -286,10 +427,9 @@ function handleStepChange(step: number) {
286
427
  :form="currentStepId"
287
428
  :loading="currentLoading"
288
429
  leading-icon="lucide:check"
289
- trailing-icon="lucide:arrow-right"
290
430
  type="submit"
291
431
  >
292
- Submit and continue
432
+ {{ currentStepSubmitLabel }}
293
433
  </UButton>
294
434
  </div>
295
435
  </div>
@@ -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'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@plutocms/supabase",
3
3
  "type": "module",
4
- "version": "0.0.1-alpha.7",
4
+ "version": "0.0.1-alpha.9",
5
5
  "main": "./nuxt.config.ts",
6
6
  "scripts": {
7
7
  "build": "nuxt build",
package/public/schema.sql CHANGED
@@ -1,14 +1,20 @@
1
1
  -- Supabase Schema SQL
2
2
  -- Settings
3
3
  --- Create enum type for settings
4
- create type TSettings as enum (
5
- 'website_title',
6
- 'website_url',
7
- 'website_description'
8
- );
4
+ do $$
5
+ begin
6
+ if not exists (select 1 from pg_type where typname = 'tsettings') then
7
+ create type TSettings as enum (
8
+ 'website_title',
9
+ 'website_url',
10
+ 'website_description'
11
+ );
12
+ end if;
13
+ end
14
+ $$;
9
15
 
10
16
  --- Create settings table
11
- create table public.settings (
17
+ create table if not exists public.settings (
12
18
  id bigint generated by default as identity not null,
13
19
  setting_name public.TSettings not null,
14
20
  setting_value text not null,
@@ -20,72 +26,160 @@ create table public.settings (
20
26
  insert into public.settings (setting_name, setting_value) values
21
27
  ('website_title', 'My Website'),
22
28
  ('website_url', 'https://mywebsite.test'),
23
- ('website_description', 'This is my website.');
29
+ ('website_description', 'This is my website.')
30
+ on conflict (setting_name) do nothing;
24
31
 
25
32
  alter table public.settings enable row level security;
26
33
 
27
34
  -- Policies for settings table
28
- create policy "Enable insert for authenticated users only"
29
- on public.settings
30
- to authenticated, dashboard_user
31
- with check (true);
35
+ do $$
36
+ begin
37
+ if not exists (
38
+ select 1 from pg_policies where tablename = 'settings' and policyname = 'Enable insert for authenticated users only'
39
+ ) then
40
+ create policy "Enable insert for authenticated users only"
41
+ on public.settings
42
+ for insert
43
+ to authenticated, dashboard_user
44
+ with check (true);
45
+ end if;
46
+ end
47
+ $$;
48
+
49
+ do $$
50
+ begin
51
+ if not exists (
52
+ select 1 from pg_policies where tablename = 'settings' and policyname = 'Enable read access for authenticated users on settings'
53
+ ) then
54
+ create policy "Enable read access for authenticated users on settings"
55
+ on public.settings
56
+ for select
57
+ to authenticated, dashboard_user
58
+ using (true);
59
+ end if;
60
+ end
61
+ $$;
62
+
63
+ do $$
64
+ begin
65
+ if not exists (
66
+ select 1 from pg_policies where tablename = 'settings' and policyname = 'Enable update for authenticated users on settings'
67
+ ) then
68
+ create policy "Enable update for authenticated users on settings"
69
+ on public.settings
70
+ for update
71
+ to authenticated, dashboard_user
72
+ using (true)
73
+ with check (true);
74
+ end if;
75
+ end
76
+ $$;
32
77
 
33
78
  --- Create enum type for healthcheck names
34
- create type Healthcheck as enum (
35
- 'first_setup'
36
- );
79
+ do $$
80
+ begin
81
+ if not exists (select 1 from pg_type where typname = 'thealthcheck') then
82
+ create type THealthcheck as enum (
83
+ 'first_setup'
84
+ );
85
+ end if;
86
+ end
87
+ $$;
37
88
 
38
89
  --- Create healthcheck table (name/value format like settings)
39
- create table public.healthcheck (
90
+ create table if not exists public.healthcheck (
40
91
  id bigint generated by default as identity not null,
41
- check_name public.Healthcheck not null,
42
- check_value text not null,
92
+ config_name public.THealthcheck not null,
93
+ config_value text not null,
43
94
  constraint healthcheck_pkey primary key (id),
44
- constraint healthcheck_check_name_key unique (check_name)
95
+ constraint healthcheck_config_name_key unique (config_name)
45
96
  ) TABLESPACE pg_default;
46
97
 
47
98
  --- Insert default healthcheck values
48
- insert into public.healthcheck (check_name, check_value) values
49
- ('first_setup', 'true');
99
+ insert into public.healthcheck (config_name, config_value) values
100
+ ('first_setup', 'true')
101
+ on conflict (config_name) do nothing;
50
102
 
51
103
  alter table public.healthcheck enable row level security;
52
104
 
53
105
  -- Allow read access to healthcheck for public and dashboard_user (no login required)
54
- create policy "Enable read access for healthcheck to public and dashboard_user"
55
- on public.healthcheck
56
- for select
57
- to public, dashboard_user
58
- using (true);
106
+ do $$
107
+ begin
108
+ if not exists (
109
+ select 1 from pg_policies where tablename = 'healthcheck' and policyname = 'Enable read access for healthcheck to public and dashboard_user'
110
+ ) then
111
+ create policy "Enable read access for healthcheck to public and dashboard_user"
112
+ on public.healthcheck
113
+ for select
114
+ to public, dashboard_user
115
+ using (true);
116
+ end if;
117
+ end
118
+ $$;
119
+
120
+ -- Allow update access to healthcheck for authenticated users and dashboard_user
121
+ do $$
122
+ begin
123
+ if not exists (
124
+ select 1 from pg_policies where tablename = 'healthcheck' and policyname = 'Enable update for authenticated users on healthcheck'
125
+ ) then
126
+ create policy "Enable update for authenticated users on healthcheck"
127
+ on public.healthcheck
128
+ for update
129
+ to authenticated, dashboard_user
130
+ using (true)
131
+ with check (true);
132
+ end if;
133
+ end
134
+ $$;
59
135
 
60
136
  -- Profiles
61
- create table profiles (
137
+ create table if not exists public.profiles (
62
138
  id uuid references auth.users on delete cascade not null primary key,
63
139
  updated_at timestamp with time zone,
64
- first_name text,
65
- last_name text,
66
- constraint first_name_length check (char_length(first_name) >= 3)
140
+ username text,
141
+ display_name text,
142
+ is_admin boolean not null default false,
143
+ constraint username_length check (char_length(username) >= 3)
67
144
  );
68
145
 
69
146
  alter table public.profiles enable row level security;
70
147
 
71
- create policy "Enable read access for authenticated users"
72
- on public.profiles
73
- to authenticated, dashboard_user
74
- for select using (true);
148
+ do $$
149
+ begin
150
+ if not exists (
151
+ select 1 from pg_policies where tablename = 'profiles' and policyname = 'Enable read access for authenticated users'
152
+ ) then
153
+ create policy "Enable read access for authenticated users"
154
+ on public.profiles
155
+ for select
156
+ to authenticated, dashboard_user
157
+ using (true);
158
+ end if;
159
+ end
160
+ $$;
75
161
 
76
162
  --- inserts a row into public.profiles
77
- create function public.handle_new_user()
163
+ create or replace function public.handle_new_user()
78
164
  returns trigger
79
165
  language plpgsql
80
166
  security definer set search_path = ''
81
167
  as $$
82
168
  begin
83
- insert into public.profiles (id, first_name, last_name)
84
- values (new.id, new.raw_user_meta_data ->> 'first_name', new.raw_user_meta_data ->> 'last_name');
169
+ insert into public.profiles (id, username, display_name)
170
+ values (new.id, new.raw_user_meta_data ->> 'username', new.raw_user_meta_data ->> 'display_name');
85
171
  return new;
86
172
  end;
87
173
  $$;
88
174
  --- trigger the function every time a user is created
89
- create trigger on_auth_user_created
90
- after insert on auth.users
91
- for each row execute procedure public.handle_new_user();
175
+ do $$
176
+ begin
177
+ if not exists (
178
+ select 1 from pg_trigger where tgname = 'on_auth_user_created'
179
+ ) then
180
+ create trigger on_auth_user_created
181
+ after insert on auth.users
182
+ for each row execute procedure public.handle_new_user();
183
+ end if;
184
+ end
185
+ $$;
@@ -7,7 +7,7 @@ interface Payload {
7
7
 
8
8
  /**
9
9
  * Splits a SQL string into individual statements, correctly handling
10
- * $$ dollar-quoted blocks (used in functions, triggers, DO blocks).
10
+ * $$ dollar-quoted blocks, single-quoted strings, and -- comments.
11
11
  */
12
12
  function splitStatements(sql: string): string[] {
13
13
  const statements: string[] = []
@@ -32,6 +32,28 @@ function splitStatements(sql: string): string[] {
32
32
  continue
33
33
  }
34
34
 
35
+ // Check for single-quoted strings (handle '' escapes)
36
+ if (sql[i] === `'`) {
37
+ current += sql[i]
38
+ i++
39
+ while (i < sql.length) {
40
+ if (sql[i] === `'` && sql[i + 1] === `'`) {
41
+ // Escaped quote
42
+ current += `''`
43
+ i += 2
44
+ continue
45
+ }
46
+ if (sql[i] === `'`) {
47
+ current += sql[i]
48
+ i++
49
+ break
50
+ }
51
+ current += sql[i]
52
+ i++
53
+ }
54
+ continue
55
+ }
56
+
35
57
  // Check for single-line comments
36
58
  if (sql[i] === '-' && sql[i + 1] === '-') {
37
59
  while (i < sql.length && sql[i] !== '\n') {
@@ -96,13 +118,6 @@ export default defineEventHandler(async (event) => {
96
118
  await sql.unsafe(statement)
97
119
  }
98
120
 
99
- // Set the first_setup flag in the healthcheck table to false
100
- await sql`
101
- UPDATE healthcheck
102
- SET check_value = 'false'
103
- WHERE check_name = 'first_setup';
104
- `
105
-
106
121
  return {
107
122
  success: true,
108
123
  message: 'Database setup completed successfully.',
@@ -1,28 +1,55 @@
1
1
  import { serverSupabaseClient } from '#supabase/server'
2
2
 
3
+ interface Payload {
4
+ username: string
5
+ display_name: string
6
+ email: string
7
+ password: string
8
+ is_admin?: boolean
9
+ }
10
+
3
11
  export default defineEventHandler(async (event) => {
4
12
  const client = await serverSupabaseClient<Database>(event)
5
13
 
6
- const body = await readBody(event)
14
+ const body = await readBody<Payload>(event)
7
15
 
8
16
  const { data, error } = await client.auth.signUp({
9
17
  email: body.email,
10
18
  password: body.password,
11
19
  options: {
12
20
  data: {
13
- first_name: body.first_name,
14
- last_name: body.last_name,
21
+ username: body.username,
22
+ display_name: body.display_name,
23
+ is_admin: body.is_admin || false,
15
24
  },
16
25
  },
17
26
  })
18
27
 
19
28
  if (error) {
20
- throw createError({
21
- statusCode: Number.parseInt(error.code ?? '500'),
22
- cause: error.cause,
29
+ return {
30
+ success: false,
23
31
  message: error.message,
24
- })
32
+ error,
33
+ }
25
34
  }
26
35
 
27
- return { message: 'User registered successfully', data }
36
+ // Set the first_setup flag in the healthcheck table to false
37
+ const { error: healthcheckError } = await client
38
+ .from('healthcheck')
39
+ .update({ config_value: 'false' })
40
+ .eq('config_name', 'first_setup')
41
+
42
+ if (healthcheckError) {
43
+ return {
44
+ success: false,
45
+ message: healthcheckError.message,
46
+ error: healthcheckError,
47
+ }
48
+ }
49
+
50
+ return {
51
+ success: true,
52
+ message: 'User registered successfully',
53
+ data,
54
+ }
28
55
  })