astro-tractstack 2.0.34 → 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
@@ -2057,10 +2057,20 @@ async function w(t, e, c) {
2057
2057
  dest: "src/middleware.ts"
2058
2058
  }
2059
2059
  ] : [],
2060
- // Multi-Tenant Types (Always included due to plan reference)
2060
+ // Manual Setup Wizard
2061
2061
  {
2062
- src: t("../templates/src/types/multiTenant.ts"),
2063
- dest: "src/types/multiTenant.ts"
2062
+ src: t("../templates/src/utils/api/setupHelpers.ts"),
2063
+ dest: "src/utils/api/setupHelpers.ts"
2064
+ },
2065
+ {
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"
2064
2074
  },
2065
2075
  // Custom Components (Conditional)
2066
2076
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.34",
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
+ }
@@ -0,0 +1,75 @@
1
+ ---
2
+ import { freshInstallStore } from '@/stores/backend';
3
+ import { preHealthCheck } from '@/utils/backend';
4
+ import SetupWizard from '@/components/storykeep/widgets/SetupWizard';
5
+
6
+ const isMultiTenant = import.meta.env.PUBLIC_ENABLE_MULTI_TENANT === 'true';
7
+
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);
13
+
14
+ const { needsSetup } = freshInstallStore.get();
15
+ if (!needsSetup) {
16
+ return Astro.redirect('/storykeep');
17
+ }
18
+ }
19
+
20
+ const isDev = import.meta.env.DEV;
21
+ const cssBasePath = '/styles';
22
+ const mainStylesUrl = isDev
23
+ ? `${cssBasePath}/storykeep.css`
24
+ : `${cssBasePath}/frontend.css`;
25
+ ---
26
+
27
+ <!doctype html>
28
+ <html lang="en" class="h-full bg-gray-50">
29
+ <head>
30
+ <meta charset="UTF-8" />
31
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
32
+ <title>Initialize TractStack</title>
33
+ <link rel="stylesheet" href={`${cssBasePath}/custom.css`} />
34
+ <link rel="stylesheet" href={mainStylesUrl} />
35
+ </head>
36
+ <body class="h-full">
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
+ }
48
+
49
+ <script>
50
+ (function initCleanSlate() {
51
+ try {
52
+ document.cookie =
53
+ 'admin_auth=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax';
54
+ document.cookie =
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';
58
+
59
+ const tractStackKeys = [];
60
+ for (let i = 0; i < localStorage.length; i++) {
61
+ const key = localStorage.key(i);
62
+ if (key && key.startsWith('tractstack_')) {
63
+ tractStackKeys.push(key);
64
+ }
65
+ }
66
+ tractStackKeys.forEach((key) => localStorage.removeItem(key));
67
+
68
+ console.log('TractStack: Clean slate initialization complete');
69
+ } catch (error) {
70
+ console.warn('TractStack: Error during clean slate init:', error);
71
+ }
72
+ })();
73
+ </script>
74
+ </body>
75
+ </html>
@@ -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
+ }
@@ -2082,6 +2082,7 @@ export async function injectTemplateFiles(
2082
2082
  src: resolve('../templates/socials/youtube.svg'),
2083
2083
  dest: 'public/socials/youtube.svg',
2084
2084
  },
2085
+
2085
2086
  // Multi-Tenant Features (Conditional)
2086
2087
  ...(config?.enableMultiTenant
2087
2088
  ? [
@@ -2092,10 +2093,21 @@ export async function injectTemplateFiles(
2092
2093
  },
2093
2094
  ]
2094
2095
  : []),
2095
- // 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
+ },
2102
+ {
2103
+ src: resolve(
2104
+ '../templates/src/components/storykeep/widgets/SetupWizard.tsx'
2105
+ ),
2106
+ dest: 'src/components/storykeep/widgets/SetupWizard.tsx',
2107
+ },
2096
2108
  {
2097
- src: resolve('../templates/src/types/multiTenant.ts'),
2098
- dest: 'src/types/multiTenant.ts',
2109
+ src: resolve('../templates/src/pages/storykeep/init.astro'),
2110
+ dest: 'src/pages/storykeep/init.astro',
2099
2111
  },
2100
2112
 
2101
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
- }