astro-tractstack 2.0.33 → 2.0.35

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.
@@ -496,8 +496,8 @@ export default defineConfig({
496
496
  integrations: [
497
497
  react(),
498
498
  tractstack({
499
- includeExamples: ${responses.includeExamples},
500
- enableMultiTenant: ${responses.enableMultiTenant},
499
+ includeExamples: ${finalResponses.includeExamples},
500
+ enableMultiTenant: ${finalResponses.enableMultiTenant},
501
501
  }),
502
502
  ],
503
503
  output: 'server',
@@ -515,8 +515,8 @@ export default defineConfig({
515
515
  integrations: [
516
516
  react(),
517
517
  tractstack({
518
- includeExamples: ${responses.includeExamples},
519
- enableMultiTenant: ${responses.enableMultiTenant},
518
+ includeExamples: ${finalResponses.includeExamples},
519
+ enableMultiTenant: ${finalResponses.enableMultiTenant},
520
520
  }),
521
521
  ],
522
522
  output: 'server',
@@ -613,10 +613,10 @@ export default defineConfig({
613
613
  // Success message
614
614
  console.log(kleur.green('\nšŸŽ‰ TractStack setup complete!'));
615
615
 
616
- const runCommand =
617
- packageManager === 'pnpm' ? 'pnpm run' : `${packageManager} run`;
616
+ //const runCommand =
617
+ // packageManager === 'pnpm' ? 'pnpm run' : `${packageManager} run`;
618
618
 
619
- if (responses.enableMultiTenant) {
619
+ if (finalResponses.enableMultiTenant) {
620
620
  console.log('\n' + kleur.bold('Multi-tenant features enabled:'));
621
621
  console.log(` • Tenant registration: ${kleur.cyan('/sandbox/register')}`);
622
622
  console.log(` • Subdomain routing middleware added`);
@@ -625,7 +625,7 @@ export default defineConfig({
625
625
  );
626
626
  }
627
627
 
628
- if (responses.includeExamples) {
628
+ if (finalResponses.includeExamples) {
629
629
  console.log(`\n${kleur.bold('Example components included:')}`);
630
630
  console.log(
631
631
  ` • Collections route: ${kleur.cyan('/collections/[param1]')}`
package/dist/index.js CHANGED
@@ -839,10 +839,6 @@ async function w(t, e, c) {
839
839
  src: t("../templates/src/pages/storykeep.astro"),
840
840
  dest: "src/pages/storykeep.astro"
841
841
  },
842
- {
843
- src: t("../templates/src/pages/storykeep/init.astro"),
844
- dest: "src/pages/storykeep/init.astro"
845
- },
846
842
  {
847
843
  src: t("../templates/src/pages/storykeep/content.astro"),
848
844
  dest: "src/pages/storykeep/content.astro"
@@ -2061,10 +2057,20 @@ async function w(t, e, c) {
2061
2057
  dest: "src/middleware.ts"
2062
2058
  }
2063
2059
  ] : [],
2064
- // Multi-Tenant Types (Always included due to plan reference)
2060
+ // Manual Setup Wizard
2061
+ {
2062
+ src: t("../templates/src/utils/api/setupHelpers.ts"),
2063
+ dest: "src/utils/api/setupHelpers.ts"
2064
+ },
2065
2065
  {
2066
- src: t("../templates/src/types/multiTenant.ts"),
2067
- dest: "src/types/multiTenant.ts"
2066
+ src: t(
2067
+ "../templates/src/components/storykeep/widgets/SetupWizard.tsx"
2068
+ ),
2069
+ dest: "src/components/storykeep/widgets/SetupWizard.tsx"
2070
+ },
2071
+ {
2072
+ src: t("../templates/src/pages/storykeep/init.astro"),
2073
+ dest: "src/pages/storykeep/init.astro"
2068
2074
  },
2069
2075
  // Custom Components (Conditional)
2070
2076
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.33",
3
+ "version": "2.0.35",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,153 @@
1
+ import { useFormState } from '@/hooks/useFormState';
2
+ import StringInput from '@/components/form/StringInput';
3
+ import BooleanToggle from '@/components/form/BooleanToggle';
4
+ import UnsavedChangesBar from '@/components/form/UnsavedChangesBar';
5
+ import {
6
+ initialSetupState,
7
+ setupStateIntercept,
8
+ validateSetup,
9
+ initializeSystem,
10
+ } from '@/utils/api/setupHelpers';
11
+
12
+ export default function SetupWizard() {
13
+ const formState = useFormState({
14
+ initialData: initialSetupState,
15
+ validator: validateSetup,
16
+ interceptor: setupStateIntercept,
17
+ onSave: async (data) => {
18
+ try {
19
+ await initializeSystem(data);
20
+ // Hard redirect to break out of any potential state/cache issues
21
+ window.location.href = '/storykeep';
22
+ return data;
23
+ } catch (error) {
24
+ console.error('Installation failed:', error);
25
+ throw error;
26
+ }
27
+ },
28
+ });
29
+
30
+ const { state, updateField, errors } = formState;
31
+
32
+ return (
33
+ <div className="mx-auto max-w-2xl p-6" style={{ paddingBottom: '112px' }}>
34
+ <div className="rounded-lg bg-white p-8 shadow-lg">
35
+ <div className="mb-8">
36
+ <div className="h-16">
37
+ <img
38
+ src="/brand/logo.svg"
39
+ className="pointer-events-none mx-auto h-full"
40
+ alt="Logo"
41
+ />
42
+ </div>
43
+
44
+ <h2 className="mb-2 mt-8 text-2xl font-bold text-gray-900">
45
+ Install Tract Stack
46
+ </h2>
47
+ <p className="text-gray-600">
48
+ Create your admin account to initialize this node.
49
+ </p>
50
+ </div>
51
+
52
+ <div className="space-y-6">
53
+ {/* Email */}
54
+ <div>
55
+ <label className="mb-1 block text-sm font-bold text-gray-700">
56
+ Email Address *
57
+ </label>
58
+ <StringInput
59
+ value={state.email}
60
+ onChange={(value) => updateField('email', value)}
61
+ type="email"
62
+ placeholder="admin@example.com"
63
+ error={errors.email}
64
+ />
65
+ <p className="mt-1 text-sm text-gray-500">
66
+ Used for system notifications and recovery.
67
+ </p>
68
+ </div>
69
+
70
+ {/* Admin Password */}
71
+ <div>
72
+ <label className="mb-1 block text-sm font-bold text-gray-700">
73
+ Admin Password *
74
+ </label>
75
+ <StringInput
76
+ value={state.adminPassword}
77
+ onChange={(value) => updateField('adminPassword', value)}
78
+ type="password"
79
+ placeholder="Strong password"
80
+ error={errors.adminPassword}
81
+ />
82
+ <p className="mt-1 text-sm text-gray-500">Minimum 8 characters</p>
83
+ </div>
84
+
85
+ {/* Confirm Password */}
86
+ <div>
87
+ <label className="mb-1 block text-sm font-bold text-gray-700">
88
+ Confirm Password *
89
+ </label>
90
+ <StringInput
91
+ value={state.confirmPassword}
92
+ onChange={(value) => updateField('confirmPassword', value)}
93
+ type="password"
94
+ placeholder="Confirm your password"
95
+ error={errors.confirmPassword}
96
+ />
97
+ </div>
98
+
99
+ {/* Database Configuration */}
100
+ <div className="rounded-lg border border-gray-200 p-4">
101
+ <div className="mb-4">
102
+ <BooleanToggle
103
+ value={state.tursoEnabled}
104
+ onChange={(value) => updateField('tursoEnabled', value)}
105
+ label="Enable Turso Database"
106
+ />
107
+ <p className="mt-2 text-sm text-gray-500">
108
+ By default, we use a local SQLite3 database. Enable this to
109
+ connect to a Turso instance.
110
+ </p>
111
+ </div>
112
+
113
+ {state.tursoEnabled && (
114
+ <div className="space-y-4 rounded-lg bg-gray-50 p-4">
115
+ <div>
116
+ <label className="mb-1 block text-sm font-bold text-gray-700">
117
+ Turso Database URL *
118
+ </label>
119
+ <StringInput
120
+ value={state.tursoDatabaseURL}
121
+ onChange={(value) => updateField('tursoDatabaseURL', value)}
122
+ placeholder="libsql://your-database.turso.io"
123
+ error={errors.tursoDatabaseURL}
124
+ />
125
+ </div>
126
+
127
+ <div>
128
+ <label className="mb-1 block text-sm font-bold text-gray-700">
129
+ Turso Auth Token *
130
+ </label>
131
+ <StringInput
132
+ value={state.tursoAuthToken}
133
+ onChange={(value) => updateField('tursoAuthToken', value)}
134
+ type="password"
135
+ placeholder="Your Turso auth token"
136
+ error={errors.tursoAuthToken}
137
+ />
138
+ </div>
139
+ </div>
140
+ )}
141
+ </div>
142
+ </div>
143
+
144
+ <UnsavedChangesBar
145
+ formState={formState}
146
+ message="Initialize System"
147
+ saveLabel="Install"
148
+ cancelLabel="Clear Form"
149
+ />
150
+ </div>
151
+ </div>
152
+ );
153
+ }
@@ -1,21 +1,24 @@
1
1
  ---
2
2
  import { freshInstallStore } from '@/stores/backend';
3
3
  import { preHealthCheck } from '@/utils/backend';
4
- import RegistrationForm from '@/components/tenant/RegistrationForm';
4
+ import SetupWizard from '@/components/storykeep/widgets/SetupWizard';
5
5
 
6
- const tenantId =
7
- Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
8
- await preHealthCheck(tenantId);
6
+ const isMultiTenant = import.meta.env.PUBLIC_ENABLE_MULTI_TENANT === 'true';
9
7
 
10
- const { needsSetup } = freshInstallStore.get();
8
+ // Only run the "Wizard Check" logic if we are NOT in multi-tenant mode.
9
+ if (!isMultiTenant) {
10
+ const tenantId =
11
+ Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
12
+ await preHealthCheck(tenantId);
11
13
 
12
- if (!needsSetup) {
13
- return Astro.redirect('/storykeep');
14
+ const { needsSetup } = freshInstallStore.get();
15
+ if (!needsSetup) {
16
+ return Astro.redirect('/storykeep');
17
+ }
14
18
  }
15
19
 
16
20
  const isDev = import.meta.env.DEV;
17
- const isInitialized = !freshInstallStore.get().needsSetup;
18
- const cssBasePath = isInitialized ? '/media/css' : '/styles';
21
+ const cssBasePath = '/styles';
19
22
  const mainStylesUrl = isDev
20
23
  ? `${cssBasePath}/storykeep.css`
21
24
  : `${cssBasePath}/frontend.css`;
@@ -31,21 +34,28 @@ const mainStylesUrl = isDev
31
34
  <link rel="stylesheet" href={mainStylesUrl} />
32
35
  </head>
33
36
  <body class="h-full">
34
- <div class="max-w-5xl p-3.5 md:p-8">
35
- <RegistrationForm client:load isInitMode={true} />
36
- </div>
37
+ {
38
+ isMultiTenant ? (
39
+ <div class="flex min-h-screen items-center justify-center">
40
+ <img src="/brand/logo.svg" class="h-16 w-auto" alt="Logo" />
41
+ </div>
42
+ ) : (
43
+ <div class="max-w-5xl p-3.5 md:p-8">
44
+ <SetupWizard client:load />
45
+ </div>
46
+ )
47
+ }
37
48
 
38
49
  <script>
39
- // Ensure clean slate for fresh installation
40
50
  (function initCleanSlate() {
41
51
  try {
42
- // Clear admin/editor auth cookies
43
52
  document.cookie =
44
53
  'admin_auth=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
45
54
  document.cookie =
46
55
  'editor_auth=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
56
+ document.cookie =
57
+ 'tractstack_session_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
47
58
 
48
- // Clear all TractStack localStorage items
49
59
  const tractStackKeys = [];
50
60
  for (let i = 0; i < localStorage.length; i++) {
51
61
  const key = localStorage.key(i);
@@ -55,16 +65,6 @@ const mainStylesUrl = isDev
55
65
  }
56
66
  tractStackKeys.forEach((key) => localStorage.removeItem(key));
57
67
 
58
- // Clear session-related items
59
- localStorage.removeItem('tractstack_session_id');
60
- localStorage.removeItem('tractstack_fingerprint');
61
- localStorage.removeItem('tractstack_visit');
62
- localStorage.removeItem('tractstack_entered_tracked');
63
-
64
- // Clear session cookie
65
- document.cookie =
66
- 'tractstack_session_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
67
-
68
68
  console.log('TractStack: Clean slate initialization complete');
69
69
  } catch (error) {
70
70
  console.warn('TractStack: Error during clean slate init:', error);
@@ -0,0 +1,114 @@
1
+ import { TractStackAPI } from '@/utils/api';
2
+
3
+ export interface SetupWizardState {
4
+ email: string;
5
+ adminPassword: string;
6
+ confirmPassword: string;
7
+ tursoEnabled: boolean;
8
+ tursoDatabaseURL: string;
9
+ tursoAuthToken: string;
10
+ }
11
+
12
+ export const initialSetupState: SetupWizardState = {
13
+ email: '',
14
+ adminPassword: '',
15
+ confirmPassword: '',
16
+ tursoEnabled: false,
17
+ tursoDatabaseURL: '',
18
+ tursoAuthToken: '',
19
+ };
20
+
21
+ /**
22
+ * State interceptor (Preserving existing UI patterns)
23
+ */
24
+ export function setupStateIntercept(
25
+ newState: SetupWizardState,
26
+ field: keyof SetupWizardState,
27
+ value: any
28
+ ): SetupWizardState {
29
+ // Pattern: Clear Turso fields when disabled
30
+ if (field === 'tursoEnabled' && !value) {
31
+ return {
32
+ ...newState,
33
+ tursoEnabled: false,
34
+ tursoDatabaseURL: '',
35
+ tursoAuthToken: '',
36
+ };
37
+ }
38
+
39
+ // Pattern: Clear confirmation password when main password changes
40
+ if (field === 'adminPassword') {
41
+ return {
42
+ ...newState,
43
+ adminPassword: value,
44
+ confirmPassword: '',
45
+ };
46
+ }
47
+
48
+ return newState;
49
+ }
50
+
51
+ /**
52
+ * Validation Logic (Preserving existing Regex and Rules)
53
+ */
54
+ export function validateSetup(state: SetupWizardState): Record<string, string> {
55
+ const errors: Record<string, string> = {};
56
+
57
+ // Email Validation pattern
58
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
59
+ if (!state.email.trim()) {
60
+ errors.email = 'Email is required';
61
+ } else if (!emailRegex.test(state.email.trim())) {
62
+ errors.email = 'Please enter a valid email address';
63
+ }
64
+
65
+ // Password Validation pattern
66
+ if (!state.adminPassword.trim()) {
67
+ errors.adminPassword = 'Admin password is required';
68
+ } else if (state.adminPassword.length < 8) {
69
+ errors.adminPassword = 'Admin password must be at least 8 characters long';
70
+ }
71
+
72
+ // Confirmation Pattern
73
+ if (!errors.adminPassword && state.adminPassword !== state.confirmPassword) {
74
+ errors.confirmPassword = 'Passwords do not match';
75
+ }
76
+
77
+ // Turso Validation Pattern
78
+ if (state.tursoEnabled) {
79
+ if (!state.tursoDatabaseURL.trim()) {
80
+ errors.tursoDatabaseURL = 'Turso Database URL is required';
81
+ } else if (!state.tursoDatabaseURL.startsWith('libsql://')) {
82
+ errors.tursoDatabaseURL =
83
+ 'Turso Database URL must start with "libsql://"';
84
+ }
85
+
86
+ if (!state.tursoAuthToken.trim()) {
87
+ errors.tursoAuthToken = 'Turso Auth Token is required';
88
+ }
89
+ }
90
+
91
+ return errors;
92
+ }
93
+
94
+ /**
95
+ * API Call (Preserving Payload Structure)
96
+ */
97
+ export async function initializeSystem(state: SetupWizardState): Promise<void> {
98
+ const api = new TractStackAPI('default');
99
+
100
+ const payload = {
101
+ adminEmail: state.email.trim(),
102
+ adminPassword: state.adminPassword.trim(),
103
+ ...(state.tursoEnabled && {
104
+ tursoDatabaseURL: state.tursoDatabaseURL.trim(),
105
+ tursoAuthToken: state.tursoAuthToken.trim(),
106
+ }),
107
+ };
108
+
109
+ const response = await api.post('/api/v1/setup/initialize', payload);
110
+
111
+ if (!response.success) {
112
+ throw new Error(response.error || 'Setup failed');
113
+ }
114
+ }
@@ -848,10 +848,6 @@ export async function injectTemplateFiles(
848
848
  src: resolve('../templates/src/pages/storykeep.astro'),
849
849
  dest: 'src/pages/storykeep.astro',
850
850
  },
851
- {
852
- src: resolve('../templates/src/pages/storykeep/init.astro'),
853
- dest: 'src/pages/storykeep/init.astro',
854
- },
855
851
  {
856
852
  src: resolve('../templates/src/pages/storykeep/content.astro'),
857
853
  dest: 'src/pages/storykeep/content.astro',
@@ -2086,6 +2082,7 @@ export async function injectTemplateFiles(
2086
2082
  src: resolve('../templates/socials/youtube.svg'),
2087
2083
  dest: 'public/socials/youtube.svg',
2088
2084
  },
2085
+
2089
2086
  // Multi-Tenant Features (Conditional)
2090
2087
  ...(config?.enableMultiTenant
2091
2088
  ? [
@@ -2096,10 +2093,21 @@ export async function injectTemplateFiles(
2096
2093
  },
2097
2094
  ]
2098
2095
  : []),
2099
- // Multi-Tenant Types (Always included due to plan reference)
2096
+
2097
+ // Manual Setup Wizard
2098
+ {
2099
+ src: resolve('../templates/src/utils/api/setupHelpers.ts'),
2100
+ dest: 'src/utils/api/setupHelpers.ts',
2101
+ },
2100
2102
  {
2101
- src: resolve('../templates/src/types/multiTenant.ts'),
2102
- dest: 'src/types/multiTenant.ts',
2103
+ src: resolve(
2104
+ '../templates/src/components/storykeep/widgets/SetupWizard.tsx'
2105
+ ),
2106
+ dest: 'src/components/storykeep/widgets/SetupWizard.tsx',
2107
+ },
2108
+ {
2109
+ src: resolve('../templates/src/pages/storykeep/init.astro'),
2110
+ dest: 'src/pages/storykeep/init.astro',
2103
2111
  },
2104
2112
 
2105
2113
  // Custom Components (Conditional)
@@ -1,77 +0,0 @@
1
- export interface TenantProvisioningData {
2
- tenantId: string;
3
- adminPassword: string;
4
- name: string;
5
- adminEmail: string;
6
- tursoEnabled: boolean;
7
- tursoDatabaseURL?: string;
8
- tursoAuthToken?: string;
9
- }
10
-
11
- export interface TenantCapacity {
12
- available: boolean;
13
- currentTenants: number;
14
- maxTenants: number;
15
- availableSlots: number;
16
- }
17
-
18
- export interface ActivationRequest {
19
- token: string;
20
- }
21
-
22
- export interface TenantActivationRequest {
23
- token: string;
24
- }
25
-
26
- export interface TenantProvisioningResponse {
27
- message: string;
28
- token: string;
29
- }
30
-
31
- export interface TenantRegistrationState {
32
- tenantId: string;
33
- adminPassword: string;
34
- confirmPassword: string;
35
- name: string;
36
- email: string;
37
- tursoEnabled: boolean;
38
- tursoDatabaseURL: string;
39
- tursoAuthToken: string;
40
- }
41
-
42
- export interface TenantValidationErrors {
43
- [key: string]: string;
44
- }
45
-
46
- // Validation functions matching backend
47
- export function validateTenantId(tenantId: string): {
48
- valid: boolean;
49
- error?: string;
50
- } {
51
- // Must be 3-12 characters
52
- if (tenantId.length < 3 || tenantId.length > 12) {
53
- return { valid: false, error: 'Tenant ID must be 3-12 characters long' };
54
- }
55
-
56
- // Must be lowercase
57
- if (tenantId !== tenantId.toLowerCase()) {
58
- return { valid: false, error: 'Tenant ID must be lowercase' };
59
- }
60
-
61
- // Only alphanumeric and dashes
62
- const validPattern = /^[a-z0-9-]+$/;
63
- if (!validPattern.test(tenantId)) {
64
- return {
65
- valid: false,
66
- error:
67
- 'Tenant ID can only contain lowercase letters, numbers, and dashes',
68
- };
69
- }
70
-
71
- // Cannot be "default" (reserved)
72
- if (tenantId === 'default') {
73
- return { valid: false, error: "'default' is a reserved tenant ID" };
74
- }
75
-
76
- return { valid: true };
77
- }