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.
- package/bin/create-tractstack.js +8 -8
- package/dist/index.js +13 -3
- package/package.json +1 -1
- package/templates/src/components/storykeep/widgets/SetupWizard.tsx +153 -0
- package/templates/src/pages/storykeep/init.astro +75 -0
- package/templates/src/utils/api/setupHelpers.ts +114 -0
- package/utils/inject-files.ts +15 -3
- package/templates/src/types/multiTenant.ts +0 -77
package/bin/create-tractstack.js
CHANGED
|
@@ -496,8 +496,8 @@ export default defineConfig({
|
|
|
496
496
|
integrations: [
|
|
497
497
|
react(),
|
|
498
498
|
tractstack({
|
|
499
|
-
includeExamples: ${
|
|
500
|
-
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: ${
|
|
519
|
-
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
|
-
|
|
616
|
+
//const runCommand =
|
|
617
|
+
// packageManager === 'pnpm' ? 'pnpm run' : `${packageManager} run`;
|
|
618
618
|
|
|
619
|
-
if (
|
|
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 (
|
|
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
|
-
//
|
|
2060
|
+
// Manual Setup Wizard
|
|
2061
2061
|
{
|
|
2062
|
-
src: t("../templates/src/
|
|
2063
|
-
dest: "src/
|
|
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
|
@@ -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
|
+
}
|
package/utils/inject-files.ts
CHANGED
|
@@ -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
|
-
|
|
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/
|
|
2098
|
-
dest: 'src/
|
|
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
|
-
}
|