astro-tractstack 2.0.31 → 2.0.32
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/dist/index.js +10 -36
- package/package.json +1 -1
- package/utils/inject-files.ts +0 -27
- package/templates/src/components/tenant/RegistrationForm.tsx +0 -449
- package/templates/src/pages/sandbox/activate.astro +0 -258
- package/templates/src/pages/sandbox/register.astro +0 -44
- package/templates/src/pages/sandbox/success.astro +0 -179
- package/templates/src/utils/api/tenantConfig.ts +0 -97
- package/templates/src/utils/api/tenantHelpers.ts +0 -172
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ function b(t) {
|
|
|
10
10
|
}
|
|
11
11
|
function g(t, e) {
|
|
12
12
|
e.info("TractStack configuration applied"), t.enableMultiTenant && e.info("Multi-tenant mode enabled"), t.includeExamples && e.info("Example components will be included");
|
|
13
|
-
const c = process.env.PUBLIC_GO_BACKEND,
|
|
13
|
+
const c = process.env.PUBLIC_GO_BACKEND, o = process.env.PUBLIC_TENANTID;
|
|
14
14
|
if (!c)
|
|
15
15
|
e.warn("PUBLIC_GO_BACKEND not set - this will be required at runtime");
|
|
16
16
|
else
|
|
@@ -19,11 +19,11 @@ function g(t, e) {
|
|
|
19
19
|
} catch {
|
|
20
20
|
e.error(`PUBLIC_GO_BACKEND is not a valid URL: ${c}`);
|
|
21
21
|
}
|
|
22
|
-
return
|
|
22
|
+
return o ? /^[a-zA-Z0-9_-]+$/.test(o) ? e.info(`Tenant ID validated: ${o}`) : e.error(`PUBLIC_TENANTID contains invalid characters: ${o}`) : e.warn("PUBLIC_TENANTID not set - this will be required at runtime"), t;
|
|
23
23
|
}
|
|
24
24
|
async function w(t, e, c) {
|
|
25
25
|
e.info("TractStack: Injecting template files");
|
|
26
|
-
const
|
|
26
|
+
const o = [
|
|
27
27
|
// Core Configuration
|
|
28
28
|
{
|
|
29
29
|
src: t("../templates/env.example"),
|
|
@@ -2053,38 +2053,12 @@ async function w(t, e, c) {
|
|
|
2053
2053
|
src: t("../templates/socials/youtube.svg"),
|
|
2054
2054
|
dest: "public/socials/youtube.svg"
|
|
2055
2055
|
},
|
|
2056
|
-
// Multi-Tenant Features
|
|
2057
|
-
{
|
|
2058
|
-
src: t("../templates/src/components/tenant/RegistrationForm.tsx"),
|
|
2059
|
-
dest: "src/components/tenant/RegistrationForm.tsx"
|
|
2060
|
-
},
|
|
2061
|
-
{
|
|
2062
|
-
src: t("../templates/src/utils/api/tenantConfig.ts"),
|
|
2063
|
-
dest: "src/utils/api/tenantConfig.ts"
|
|
2064
|
-
},
|
|
2065
|
-
{
|
|
2066
|
-
src: t("../templates/src/utils/api/tenantHelpers.ts"),
|
|
2067
|
-
dest: "src/utils/api/tenantHelpers.ts"
|
|
2068
|
-
},
|
|
2069
2056
|
// Multi-Tenant Features (Conditional)
|
|
2070
2057
|
...c?.enableMultiTenant ? [
|
|
2071
2058
|
// Middleware
|
|
2072
2059
|
{
|
|
2073
2060
|
src: t("../templates/src/middleware.ts"),
|
|
2074
2061
|
dest: "src/middleware.ts"
|
|
2075
|
-
},
|
|
2076
|
-
// Pages
|
|
2077
|
-
{
|
|
2078
|
-
src: t("../templates/src/pages/sandbox/register.astro"),
|
|
2079
|
-
dest: "src/pages/sandbox/register.astro"
|
|
2080
|
-
},
|
|
2081
|
-
{
|
|
2082
|
-
src: t("../templates/src/pages/sandbox/activate.astro"),
|
|
2083
|
-
dest: "src/pages/sandbox/activate.astro"
|
|
2084
|
-
},
|
|
2085
|
-
{
|
|
2086
|
-
src: t("../templates/src/pages/sandbox/success.astro"),
|
|
2087
|
-
dest: "src/pages/sandbox/success.astro"
|
|
2088
2062
|
}
|
|
2089
2063
|
] : [],
|
|
2090
2064
|
// Multi-Tenant Types (Always included due to plan reference)
|
|
@@ -2164,12 +2138,12 @@ async function w(t, e, c) {
|
|
|
2164
2138
|
}
|
|
2165
2139
|
] : []
|
|
2166
2140
|
];
|
|
2167
|
-
for (const s of
|
|
2141
|
+
for (const s of o)
|
|
2168
2142
|
try {
|
|
2169
2143
|
const p = i(s.dest);
|
|
2170
2144
|
n(p) || x(p, { recursive: !0 });
|
|
2171
|
-
const
|
|
2172
|
-
if (!n(s.dest) ||
|
|
2145
|
+
const r = !s.protected && (s.dest === "tailwind.config.cjs" || s.dest.startsWith("src/components/codehooks/") || s.dest.startsWith("src/components/widgets/") || s.dest.startsWith("src/") || s.dest.startsWith("public/client/") || s.dest === ".gitignore");
|
|
2146
|
+
if (!n(s.dest) || r)
|
|
2173
2147
|
if (n(s.src))
|
|
2174
2148
|
k(s.src, s.dest), e.info(`Updated ${s.dest}`);
|
|
2175
2149
|
else {
|
|
@@ -2178,8 +2152,8 @@ async function w(t, e, c) {
|
|
|
2178
2152
|
}
|
|
2179
2153
|
else s.protected ? e.info(`Protected: ${s.dest} (skipped overwrite)`) : e.info(`Skipped existing ${s.dest}`);
|
|
2180
2154
|
} catch (p) {
|
|
2181
|
-
const
|
|
2182
|
-
e.error(`Failed to create ${s.dest}: ${
|
|
2155
|
+
const r = p instanceof Error ? p.message : String(p);
|
|
2156
|
+
e.error(`Failed to create ${s.dest}: ${r}`);
|
|
2183
2157
|
}
|
|
2184
2158
|
}
|
|
2185
2159
|
function _(t) {
|
|
@@ -2198,7 +2172,7 @@ function C(t = {}) {
|
|
|
2198
2172
|
return {
|
|
2199
2173
|
name: "astro-tractstack",
|
|
2200
2174
|
hooks: {
|
|
2201
|
-
"astro:config:setup": async ({ config: c, updateConfig:
|
|
2175
|
+
"astro:config:setup": async ({ config: c, updateConfig: o, logger: s }) => {
|
|
2202
2176
|
g(t, s);
|
|
2203
2177
|
const p = t.enableMultiTenant || !1;
|
|
2204
2178
|
if (s.info(
|
|
@@ -2218,7 +2192,7 @@ function C(t = {}) {
|
|
|
2218
2192
|
), new Error(
|
|
2219
2193
|
"TractStack requires an SSR adapter. Please add @astrojs/node adapter to your astro.config.mjs"
|
|
2220
2194
|
);
|
|
2221
|
-
|
|
2195
|
+
o({
|
|
2222
2196
|
vite: {
|
|
2223
2197
|
define: {
|
|
2224
2198
|
__TRACTSTACK_VERSION__: JSON.stringify("2.0.0-alpha.1"),
|
package/package.json
CHANGED
package/utils/inject-files.ts
CHANGED
|
@@ -2086,20 +2086,6 @@ export async function injectTemplateFiles(
|
|
|
2086
2086
|
src: resolve('../templates/socials/youtube.svg'),
|
|
2087
2087
|
dest: 'public/socials/youtube.svg',
|
|
2088
2088
|
},
|
|
2089
|
-
|
|
2090
|
-
// Multi-Tenant Features
|
|
2091
|
-
{
|
|
2092
|
-
src: resolve('../templates/src/components/tenant/RegistrationForm.tsx'),
|
|
2093
|
-
dest: 'src/components/tenant/RegistrationForm.tsx',
|
|
2094
|
-
},
|
|
2095
|
-
{
|
|
2096
|
-
src: resolve('../templates/src/utils/api/tenantConfig.ts'),
|
|
2097
|
-
dest: 'src/utils/api/tenantConfig.ts',
|
|
2098
|
-
},
|
|
2099
|
-
{
|
|
2100
|
-
src: resolve('../templates/src/utils/api/tenantHelpers.ts'),
|
|
2101
|
-
dest: 'src/utils/api/tenantHelpers.ts',
|
|
2102
|
-
},
|
|
2103
2089
|
// Multi-Tenant Features (Conditional)
|
|
2104
2090
|
...(config?.enableMultiTenant
|
|
2105
2091
|
? [
|
|
@@ -2108,19 +2094,6 @@ export async function injectTemplateFiles(
|
|
|
2108
2094
|
src: resolve('../templates/src/middleware.ts'),
|
|
2109
2095
|
dest: 'src/middleware.ts',
|
|
2110
2096
|
},
|
|
2111
|
-
// Pages
|
|
2112
|
-
{
|
|
2113
|
-
src: resolve('../templates/src/pages/sandbox/register.astro'),
|
|
2114
|
-
dest: 'src/pages/sandbox/register.astro',
|
|
2115
|
-
},
|
|
2116
|
-
{
|
|
2117
|
-
src: resolve('../templates/src/pages/sandbox/activate.astro'),
|
|
2118
|
-
dest: 'src/pages/sandbox/activate.astro',
|
|
2119
|
-
},
|
|
2120
|
-
{
|
|
2121
|
-
src: resolve('../templates/src/pages/sandbox/success.astro'),
|
|
2122
|
-
dest: 'src/pages/sandbox/success.astro',
|
|
2123
|
-
},
|
|
2124
2097
|
]
|
|
2125
2098
|
: []),
|
|
2126
2099
|
// Multi-Tenant Types (Always included due to plan reference)
|
|
@@ -1,449 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
2
|
-
import { useFormState } from '@/hooks/useFormState';
|
|
3
|
-
import {
|
|
4
|
-
convertToLocalState,
|
|
5
|
-
convertToBackendFormat,
|
|
6
|
-
validateTenantRegistration,
|
|
7
|
-
tenantStateIntercept,
|
|
8
|
-
} from '@/utils/api/tenantHelpers';
|
|
9
|
-
import { checkTenantCapacity, provisionTenant } from '@/utils/api/tenantConfig';
|
|
10
|
-
import { TractStackAPI } from '@/utils/api';
|
|
11
|
-
import UnsavedChangesBar from '@/components/form/UnsavedChangesBar';
|
|
12
|
-
import StringInput from '@/components/form/StringInput';
|
|
13
|
-
import BooleanToggle from '@/components/form/BooleanToggle';
|
|
14
|
-
import type { TenantCapacity } from '@/types/multiTenant';
|
|
15
|
-
import type { TenantRegistrationState } from '@/types/multiTenant';
|
|
16
|
-
|
|
17
|
-
interface RegistrationFormProps {
|
|
18
|
-
isInitMode?: boolean;
|
|
19
|
-
onSuccess?: (tenantId?: string) => void;
|
|
20
|
-
onCapacityFull?: () => void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export default function RegistrationForm({
|
|
24
|
-
isInitMode = false,
|
|
25
|
-
onSuccess,
|
|
26
|
-
onCapacityFull,
|
|
27
|
-
}: RegistrationFormProps) {
|
|
28
|
-
const [capacity, setCapacity] = useState<TenantCapacity | null>(null);
|
|
29
|
-
const [loadingCapacity, setLoadingCapacity] = useState(!isInitMode);
|
|
30
|
-
const [capacityError, setCapacityError] = useState<string | null>(null);
|
|
31
|
-
|
|
32
|
-
// Modal state for tenant preparation
|
|
33
|
-
const [showPreparationModal, setShowPreparationModal] = useState(false);
|
|
34
|
-
const [activationToken, setActivationToken] = useState<string>('');
|
|
35
|
-
const [preparingTenantId, setPreparingTenantId] = useState<string>('');
|
|
36
|
-
|
|
37
|
-
// Load capacity information on mount
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
if (isInitMode) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const loadCapacity = async () => {
|
|
44
|
-
try {
|
|
45
|
-
const capacityData = await checkTenantCapacity('default');
|
|
46
|
-
setCapacity(capacityData);
|
|
47
|
-
|
|
48
|
-
// Check if at capacity
|
|
49
|
-
if (!capacityData.available) {
|
|
50
|
-
onCapacityFull?.();
|
|
51
|
-
}
|
|
52
|
-
} catch (error) {
|
|
53
|
-
setCapacityError(
|
|
54
|
-
error instanceof Error ? error.message : 'Failed to load capacity'
|
|
55
|
-
);
|
|
56
|
-
} finally {
|
|
57
|
-
setLoadingCapacity(false);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
loadCapacity();
|
|
62
|
-
}, [onCapacityFull, isInitMode]);
|
|
63
|
-
|
|
64
|
-
const initialState: TenantRegistrationState = convertToLocalState();
|
|
65
|
-
|
|
66
|
-
const formState = useFormState({
|
|
67
|
-
initialData: initialState,
|
|
68
|
-
validator: (data) =>
|
|
69
|
-
validateTenantRegistration(data, undefined, isInitMode),
|
|
70
|
-
interceptor: tenantStateIntercept,
|
|
71
|
-
onSave: async (data) => {
|
|
72
|
-
try {
|
|
73
|
-
if (isInitMode) {
|
|
74
|
-
const api = new TractStackAPI('default');
|
|
75
|
-
const setupData = {
|
|
76
|
-
adminEmail: data.email.trim(),
|
|
77
|
-
adminPassword: data.adminPassword.trim(),
|
|
78
|
-
...(data.tursoEnabled && {
|
|
79
|
-
tursoDatabaseURL: data.tursoDatabaseURL.trim(),
|
|
80
|
-
tursoAuthToken: data.tursoAuthToken.trim(),
|
|
81
|
-
}),
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const response = await api.post(
|
|
85
|
-
'/api/v1/setup/initialize',
|
|
86
|
-
setupData
|
|
87
|
-
);
|
|
88
|
-
if (!response.success) {
|
|
89
|
-
throw new Error(response.error || 'Setup failed');
|
|
90
|
-
}
|
|
91
|
-
window.location.href = '/storykeep';
|
|
92
|
-
return data;
|
|
93
|
-
} else {
|
|
94
|
-
const backendData = convertToBackendFormat(data);
|
|
95
|
-
const result = await provisionTenant('default', backendData);
|
|
96
|
-
|
|
97
|
-
// Store the activation token and tenant ID for the modal
|
|
98
|
-
setActivationToken(result.token);
|
|
99
|
-
setPreparingTenantId(data.tenantId);
|
|
100
|
-
setShowPreparationModal(true);
|
|
101
|
-
|
|
102
|
-
return data;
|
|
103
|
-
}
|
|
104
|
-
} catch (error) {
|
|
105
|
-
console.error('Tenant provisioning failed:', error);
|
|
106
|
-
throw error;
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
const { state, updateField, errors } = formState;
|
|
112
|
-
|
|
113
|
-
// Handle activation button click in modal
|
|
114
|
-
const handleActivate = () => {
|
|
115
|
-
if (activationToken && preparingTenantId) {
|
|
116
|
-
// Check if we're in dev mode
|
|
117
|
-
const isDev = import.meta.env.DEV;
|
|
118
|
-
|
|
119
|
-
if (isDev) {
|
|
120
|
-
// In dev mode, navigate to local activation page with tenant ID param
|
|
121
|
-
window.location.href = `/sandbox/activate?token=${activationToken}&tenantId=${preparingTenantId}`;
|
|
122
|
-
} else {
|
|
123
|
-
// In production, use the full subdomain URL
|
|
124
|
-
window.location.href = `https://${preparingTenantId}.sandbox.freewebpress.com/sandbox/activate?token=${activationToken}`;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// Handle modal close
|
|
130
|
-
const handleModalClose = () => {
|
|
131
|
-
setShowPreparationModal(false);
|
|
132
|
-
onSuccess?.(preparingTenantId);
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
if (loadingCapacity) {
|
|
136
|
-
return (
|
|
137
|
-
<div className="mx-auto max-w-2xl p-6">
|
|
138
|
-
<div className="animate-pulse">
|
|
139
|
-
<div className="mb-4 h-8 rounded bg-gray-200"></div>
|
|
140
|
-
<div className="mb-2 h-4 rounded bg-gray-200"></div>
|
|
141
|
-
<div className="mb-4 h-4 rounded bg-gray-200"></div>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (capacityError) {
|
|
148
|
-
return (
|
|
149
|
-
<div className="mx-auto max-w-2xl p-6">
|
|
150
|
-
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
|
|
151
|
-
<h3 className="mb-2 text-lg font-bold text-red-800">
|
|
152
|
-
Unable to Load Registration
|
|
153
|
-
</h3>
|
|
154
|
-
<p className="text-red-700">{capacityError}</p>
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (!capacity && !isInitMode) {
|
|
161
|
-
return (
|
|
162
|
-
<div className="mx-auto max-w-2xl p-6">
|
|
163
|
-
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4">
|
|
164
|
-
<h3 className="mb-2 text-lg font-bold text-yellow-800">
|
|
165
|
-
Registration Currently Unavailable
|
|
166
|
-
</h3>
|
|
167
|
-
<p className="text-yellow-700">
|
|
168
|
-
We've reached our current capacity. Please check back later for
|
|
169
|
-
availability.
|
|
170
|
-
</p>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return (
|
|
177
|
-
<>
|
|
178
|
-
<div className="mx-auto max-w-2xl p-6" style={{ paddingBottom: '112px' }}>
|
|
179
|
-
<div className="rounded-lg bg-white p-8 shadow-lg">
|
|
180
|
-
<div className="mb-8">
|
|
181
|
-
<div className="h-16">
|
|
182
|
-
<img
|
|
183
|
-
src="/brand/logo.svg"
|
|
184
|
-
className="pointer-events-none mx-auto h-full"
|
|
185
|
-
alt="Logo"
|
|
186
|
-
/>
|
|
187
|
-
</div>
|
|
188
|
-
|
|
189
|
-
<h2 className="mb-2 mt-8 text-2xl font-bold text-gray-900">
|
|
190
|
-
{isInitMode ? `Install Tract Stack` : `Try Tract Stack`}
|
|
191
|
-
</h2>
|
|
192
|
-
{!isInitMode && (
|
|
193
|
-
<p className="text-gray-600">
|
|
194
|
-
Set up your free sandbox environment to try TractStack.
|
|
195
|
-
</p>
|
|
196
|
-
)}
|
|
197
|
-
{!isInitMode && capacity && (
|
|
198
|
-
<div className="mt-4 rounded-lg bg-orange-50 p-4">
|
|
199
|
-
<div className="flex">
|
|
200
|
-
<div className="flex-shrink-0">
|
|
201
|
-
<svg
|
|
202
|
-
className="h-5 w-5 text-orange-400"
|
|
203
|
-
viewBox="0 0 20 20"
|
|
204
|
-
fill="currentColor"
|
|
205
|
-
>
|
|
206
|
-
<path
|
|
207
|
-
fillRule="evenodd"
|
|
208
|
-
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
|
209
|
-
clipRule="evenodd"
|
|
210
|
-
/>
|
|
211
|
-
</svg>
|
|
212
|
-
</div>
|
|
213
|
-
<div className="ml-3">
|
|
214
|
-
<p className="text-sm text-orange-700">
|
|
215
|
-
{capacity.availableSlots} of {capacity.maxTenants} slots
|
|
216
|
-
remaining
|
|
217
|
-
</p>
|
|
218
|
-
</div>
|
|
219
|
-
</div>
|
|
220
|
-
</div>
|
|
221
|
-
)}
|
|
222
|
-
</div>
|
|
223
|
-
|
|
224
|
-
<div className="space-y-6">
|
|
225
|
-
{/* Tenant ID field - hidden in init mode */}
|
|
226
|
-
{!isInitMode && (
|
|
227
|
-
<div>
|
|
228
|
-
<label className="mb-1 block text-sm font-bold text-gray-700">
|
|
229
|
-
Tenant ID *
|
|
230
|
-
</label>
|
|
231
|
-
<StringInput
|
|
232
|
-
value={state.tenantId}
|
|
233
|
-
onChange={(value) => updateField('tenantId', value)}
|
|
234
|
-
placeholder="my-awesome-tenant"
|
|
235
|
-
error={errors.tenantId}
|
|
236
|
-
/>
|
|
237
|
-
<p className="mt-1 text-sm text-gray-500">
|
|
238
|
-
3-12 characters, lowercase letters, numbers, and dashes only
|
|
239
|
-
</p>
|
|
240
|
-
</div>
|
|
241
|
-
)}
|
|
242
|
-
|
|
243
|
-
{/* Admin Password */}
|
|
244
|
-
<div>
|
|
245
|
-
<label className="mb-1 block text-sm font-bold text-gray-700">
|
|
246
|
-
Admin Password *
|
|
247
|
-
</label>
|
|
248
|
-
<StringInput
|
|
249
|
-
value={state.adminPassword}
|
|
250
|
-
onChange={(value) => updateField('adminPassword', value)}
|
|
251
|
-
type="password"
|
|
252
|
-
placeholder="Strong password for admin access"
|
|
253
|
-
error={errors.adminPassword}
|
|
254
|
-
/>
|
|
255
|
-
<p className="mt-1 text-sm text-gray-500">Minimum 8 characters</p>
|
|
256
|
-
</div>
|
|
257
|
-
|
|
258
|
-
{/* Confirm Password */}
|
|
259
|
-
<div>
|
|
260
|
-
<label className="mb-1 block text-sm font-bold text-gray-700">
|
|
261
|
-
Confirm Password *
|
|
262
|
-
</label>
|
|
263
|
-
<StringInput
|
|
264
|
-
value={state.confirmPassword}
|
|
265
|
-
onChange={(value) => updateField('confirmPassword', value)}
|
|
266
|
-
type="password"
|
|
267
|
-
placeholder="Confirm your admin password"
|
|
268
|
-
error={errors.confirmPassword}
|
|
269
|
-
/>
|
|
270
|
-
</div>
|
|
271
|
-
|
|
272
|
-
{/* Name */}
|
|
273
|
-
<div>
|
|
274
|
-
<label className="mb-1 block text-sm font-bold text-gray-700">
|
|
275
|
-
Your Name *
|
|
276
|
-
</label>
|
|
277
|
-
<StringInput
|
|
278
|
-
value={state.name}
|
|
279
|
-
onChange={(value) => updateField('name', value)}
|
|
280
|
-
placeholder="John Doe"
|
|
281
|
-
error={errors.name}
|
|
282
|
-
/>
|
|
283
|
-
</div>
|
|
284
|
-
|
|
285
|
-
{/* Email */}
|
|
286
|
-
<div>
|
|
287
|
-
<label className="mb-1 block text-sm font-bold text-gray-700">
|
|
288
|
-
Email Address *
|
|
289
|
-
</label>
|
|
290
|
-
<StringInput
|
|
291
|
-
value={state.email}
|
|
292
|
-
onChange={(value) => updateField('email', value)}
|
|
293
|
-
type="email"
|
|
294
|
-
placeholder="susie@amazing.com"
|
|
295
|
-
error={errors.email}
|
|
296
|
-
/>
|
|
297
|
-
<p className="mt-1 text-sm text-gray-500">
|
|
298
|
-
{isInitMode
|
|
299
|
-
? `Used for password reset, etc.`
|
|
300
|
-
: `You'll receive an activation email at this address`}
|
|
301
|
-
</p>
|
|
302
|
-
</div>
|
|
303
|
-
|
|
304
|
-
{/* Database Configuration */}
|
|
305
|
-
<div className="rounded-lg border border-gray-200 p-4">
|
|
306
|
-
<div className="mb-4">
|
|
307
|
-
<BooleanToggle
|
|
308
|
-
value={state.tursoEnabled}
|
|
309
|
-
onChange={(value) => updateField('tursoEnabled', value)}
|
|
310
|
-
label="Enable Turso Database"
|
|
311
|
-
/>
|
|
312
|
-
<p className="mt-2 text-sm text-gray-500">
|
|
313
|
-
By default, your tenant will use SQLite3. Enable this option
|
|
314
|
-
to use your own Turso database instead.
|
|
315
|
-
</p>
|
|
316
|
-
</div>
|
|
317
|
-
|
|
318
|
-
{state.tursoEnabled && (
|
|
319
|
-
<div className="space-y-4 rounded-lg bg-gray-50 p-4">
|
|
320
|
-
<div>
|
|
321
|
-
<label className="mb-1 block text-sm font-bold text-gray-700">
|
|
322
|
-
Turso Database URL *
|
|
323
|
-
</label>
|
|
324
|
-
<StringInput
|
|
325
|
-
value={state.tursoDatabaseURL}
|
|
326
|
-
onChange={(value) =>
|
|
327
|
-
updateField('tursoDatabaseURL', value)
|
|
328
|
-
}
|
|
329
|
-
placeholder="libsql://your-database.turso.io"
|
|
330
|
-
error={errors.tursoDatabaseURL}
|
|
331
|
-
/>
|
|
332
|
-
<p className="mt-1 text-sm text-gray-500">
|
|
333
|
-
Must start with libsql://
|
|
334
|
-
</p>
|
|
335
|
-
</div>
|
|
336
|
-
|
|
337
|
-
<div>
|
|
338
|
-
<label className="mb-1 block text-sm font-bold text-gray-700">
|
|
339
|
-
Turso Auth Token *
|
|
340
|
-
</label>
|
|
341
|
-
<StringInput
|
|
342
|
-
value={state.tursoAuthToken}
|
|
343
|
-
onChange={(value) => updateField('tursoAuthToken', value)}
|
|
344
|
-
type="password"
|
|
345
|
-
placeholder="Your Turso auth token"
|
|
346
|
-
error={errors.tursoAuthToken}
|
|
347
|
-
/>
|
|
348
|
-
</div>
|
|
349
|
-
</div>
|
|
350
|
-
)}
|
|
351
|
-
</div>
|
|
352
|
-
</div>
|
|
353
|
-
|
|
354
|
-
<UnsavedChangesBar
|
|
355
|
-
formState={formState}
|
|
356
|
-
message={
|
|
357
|
-
isInitMode
|
|
358
|
-
? `Install Tract Stack`
|
|
359
|
-
: `Complete your tenant registration`
|
|
360
|
-
}
|
|
361
|
-
saveLabel={isInitMode ? `Install` : `Create Tenant`}
|
|
362
|
-
cancelLabel="Clear Form"
|
|
363
|
-
/>
|
|
364
|
-
</div>
|
|
365
|
-
</div>
|
|
366
|
-
|
|
367
|
-
{/* Tenant Preparation Modal */}
|
|
368
|
-
{showPreparationModal && (
|
|
369
|
-
<div className="fixed inset-0 z-50 overflow-y-auto">
|
|
370
|
-
<div className="flex min-h-screen items-center justify-center p-4">
|
|
371
|
-
<div
|
|
372
|
-
className="fixed inset-0 bg-black opacity-25"
|
|
373
|
-
onClick={handleModalClose}
|
|
374
|
-
/>
|
|
375
|
-
|
|
376
|
-
<div className="relative w-full max-w-lg rounded-lg bg-white shadow-xl">
|
|
377
|
-
<div className="p-6">
|
|
378
|
-
<div className="mb-6 text-center">
|
|
379
|
-
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
|
380
|
-
<svg
|
|
381
|
-
className="h-6 w-6 text-green-600"
|
|
382
|
-
fill="none"
|
|
383
|
-
viewBox="0 0 24 24"
|
|
384
|
-
strokeWidth="1.5"
|
|
385
|
-
stroke="currentColor"
|
|
386
|
-
>
|
|
387
|
-
<path
|
|
388
|
-
strokeLinecap="round"
|
|
389
|
-
strokeLinejoin="round"
|
|
390
|
-
d="M4.5 12.75l6 6 9-13.5"
|
|
391
|
-
/>
|
|
392
|
-
</svg>
|
|
393
|
-
</div>
|
|
394
|
-
<h3 className="text-xl font-bold text-gray-900">
|
|
395
|
-
Your TractStack is Being Prepared
|
|
396
|
-
</h3>
|
|
397
|
-
<p className="mt-2 text-sm text-gray-600">
|
|
398
|
-
We've successfully provisioned your tenant{' '}
|
|
399
|
-
<strong>{preparingTenantId}</strong>. Click the button below
|
|
400
|
-
to activate your sandbox environment.
|
|
401
|
-
</p>
|
|
402
|
-
</div>
|
|
403
|
-
|
|
404
|
-
<div className="mb-6 rounded-lg bg-orange-50 p-4">
|
|
405
|
-
<div className="flex">
|
|
406
|
-
<div className="flex-shrink-0">
|
|
407
|
-
<svg
|
|
408
|
-
className="h-5 w-5 text-orange-400"
|
|
409
|
-
viewBox="0 0 20 20"
|
|
410
|
-
fill="currentColor"
|
|
411
|
-
>
|
|
412
|
-
<path
|
|
413
|
-
fillRule="evenodd"
|
|
414
|
-
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
|
415
|
-
clipRule="evenodd"
|
|
416
|
-
/>
|
|
417
|
-
</svg>
|
|
418
|
-
</div>
|
|
419
|
-
<div className="ml-3">
|
|
420
|
-
<p className="text-sm text-orange-700">
|
|
421
|
-
You'll also receive an email with this activation link
|
|
422
|
-
at <strong>{state.email}</strong>
|
|
423
|
-
</p>
|
|
424
|
-
</div>
|
|
425
|
-
</div>
|
|
426
|
-
</div>
|
|
427
|
-
|
|
428
|
-
<div className="flex justify-end space-x-3">
|
|
429
|
-
<button
|
|
430
|
-
onClick={handleModalClose}
|
|
431
|
-
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
|
432
|
-
>
|
|
433
|
-
I'll Use the Email Link
|
|
434
|
-
</button>
|
|
435
|
-
<button
|
|
436
|
-
onClick={handleActivate}
|
|
437
|
-
className="rounded-md border border-transparent bg-cyan-600 px-4 py-2 text-sm font-bold text-white hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
|
438
|
-
>
|
|
439
|
-
Activate Now
|
|
440
|
-
</button>
|
|
441
|
-
</div>
|
|
442
|
-
</div>
|
|
443
|
-
</div>
|
|
444
|
-
</div>
|
|
445
|
-
</div>
|
|
446
|
-
)}
|
|
447
|
-
</>
|
|
448
|
-
);
|
|
449
|
-
}
|
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import Layout from '@/layouts/Layout.astro';
|
|
3
|
-
import { getBrandConfig } from '@/utils/api/brandConfig';
|
|
4
|
-
import { preHealthCheck } from '@/utils/backend';
|
|
5
|
-
|
|
6
|
-
const tenantId = 'default'; // For registration, always use default
|
|
7
|
-
|
|
8
|
-
const healthCheckRedirect = await preHealthCheck(tenantId);
|
|
9
|
-
if (healthCheckRedirect !== undefined) {
|
|
10
|
-
return healthCheckRedirect;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const goBackend = import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
14
|
-
|
|
15
|
-
// Check if multi-tenant is enabled
|
|
16
|
-
const isMultiTenantEnabled =
|
|
17
|
-
import.meta.env.PUBLIC_ENABLE_MULTI_TENANT === 'true';
|
|
18
|
-
if (!isMultiTenantEnabled) {
|
|
19
|
-
return Astro.redirect('/?error=multi-tenant-disabled');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Dev mode detection
|
|
23
|
-
const isDev = import.meta.env.DEV;
|
|
24
|
-
|
|
25
|
-
// Get activation token from URL
|
|
26
|
-
const url = new URL(Astro.request.url);
|
|
27
|
-
const token = url.searchParams.get('token');
|
|
28
|
-
|
|
29
|
-
let activationResult = {
|
|
30
|
-
success: false,
|
|
31
|
-
error: null as string | null,
|
|
32
|
-
tenantId: null as string | null,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
if (!token) {
|
|
36
|
-
activationResult.error =
|
|
37
|
-
'Missing activation token. Please check your email link.';
|
|
38
|
-
console.log('ERROR: No token provided');
|
|
39
|
-
} else {
|
|
40
|
-
// Attempt activation
|
|
41
|
-
try {
|
|
42
|
-
// Get tenantId BEFORE calling activation
|
|
43
|
-
let tenantId: string;
|
|
44
|
-
|
|
45
|
-
if (isDev) {
|
|
46
|
-
// In dev mode, get tenant ID from URL param or use default
|
|
47
|
-
tenantId = url.searchParams.get('tenantId') || 'localhost';
|
|
48
|
-
} else {
|
|
49
|
-
// Extract tenant ID from current domain in production
|
|
50
|
-
const hostname = Astro.request.headers.get('host') || '';
|
|
51
|
-
const parts = hostname.split('.');
|
|
52
|
-
if (parts.length >= 4 && parts[1] === 'sandbox') {
|
|
53
|
-
tenantId = parts[0];
|
|
54
|
-
} else {
|
|
55
|
-
throw new Error('Could not determine tenant ID from domain');
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Make direct fetch call to Go backend (like other .astro files)
|
|
60
|
-
const response = await fetch(`${goBackend}/api/v1/tenant/activation`, {
|
|
61
|
-
method: 'POST',
|
|
62
|
-
headers: {
|
|
63
|
-
'Content-Type': 'application/json',
|
|
64
|
-
'X-Tenant-ID': tenantId,
|
|
65
|
-
},
|
|
66
|
-
body: JSON.stringify({ token }),
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const responseText = await response.text();
|
|
70
|
-
|
|
71
|
-
if (response.ok) {
|
|
72
|
-
activationResult.success = true;
|
|
73
|
-
activationResult.tenantId = tenantId;
|
|
74
|
-
} else {
|
|
75
|
-
let errorData;
|
|
76
|
-
try {
|
|
77
|
-
errorData = JSON.parse(responseText);
|
|
78
|
-
} catch (e) {
|
|
79
|
-
errorData = { error: responseText };
|
|
80
|
-
}
|
|
81
|
-
activationResult.error = errorData.error || 'Activation failed';
|
|
82
|
-
console.log('FAILED: Activation error:', activationResult.error);
|
|
83
|
-
}
|
|
84
|
-
} catch (error) {
|
|
85
|
-
activationResult.error =
|
|
86
|
-
error instanceof Error ? error.message : 'Activation failed';
|
|
87
|
-
console.log('EXCEPTION:', error);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const brandConfig = await getBrandConfig('default');
|
|
92
|
-
const title = activationResult.success
|
|
93
|
-
? 'Tenant Activated | TractStack'
|
|
94
|
-
: 'Activation Error | TractStack';
|
|
95
|
-
|
|
96
|
-
// Build dashboard URL based on environment
|
|
97
|
-
const getDashboardUrl = (tenantId: string) => {
|
|
98
|
-
if (isDev) {
|
|
99
|
-
return '/storykeep';
|
|
100
|
-
}
|
|
101
|
-
return `https://${tenantId}.sandbox.freewebpress.com/storykeep`;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// Build display URL based on environment
|
|
105
|
-
const getDisplayUrl = (tenantId: string) => {
|
|
106
|
-
if (isDev) {
|
|
107
|
-
return 'localhost:4321';
|
|
108
|
-
}
|
|
109
|
-
return `https://${tenantId}.sandbox.freewebpress.com`;
|
|
110
|
-
};
|
|
111
|
-
---
|
|
112
|
-
|
|
113
|
-
<Layout title={title} slug="tenant-activate" brandConfig={brandConfig}>
|
|
114
|
-
<main
|
|
115
|
-
id="main-content"
|
|
116
|
-
class="flex min-h-screen items-center justify-center bg-gray-50"
|
|
117
|
-
>
|
|
118
|
-
<div class="mx-auto max-w-2xl p-6">
|
|
119
|
-
{
|
|
120
|
-
activationResult.success ? (
|
|
121
|
-
<div class="rounded-lg bg-white p-8 text-center shadow-lg">
|
|
122
|
-
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-green-100">
|
|
123
|
-
<svg
|
|
124
|
-
class="h-8 w-8 text-green-600"
|
|
125
|
-
fill="none"
|
|
126
|
-
stroke="currentColor"
|
|
127
|
-
viewBox="0 0 24 24"
|
|
128
|
-
>
|
|
129
|
-
<path
|
|
130
|
-
stroke-linecap="round"
|
|
131
|
-
stroke-linejoin="round"
|
|
132
|
-
stroke-width="2"
|
|
133
|
-
d="M5 13l4 4L19 7"
|
|
134
|
-
/>
|
|
135
|
-
</svg>
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
<h2 class="mb-4 text-2xl font-bold text-gray-900">
|
|
139
|
-
🎉 Tenant Activated Successfully!
|
|
140
|
-
</h2>
|
|
141
|
-
|
|
142
|
-
<p class="mb-6 text-gray-600">
|
|
143
|
-
Your TractStack tenant has been activated and is ready to use.
|
|
144
|
-
</p>
|
|
145
|
-
|
|
146
|
-
{activationResult.tenantId && (
|
|
147
|
-
<div class="mb-6 rounded-lg border border-orange-200 bg-orange-50 p-4">
|
|
148
|
-
<p class="mb-2 text-sm font-bold text-orange-800">
|
|
149
|
-
Your tenant URL:
|
|
150
|
-
</p>
|
|
151
|
-
<p class="break-all font-mono text-orange-700">
|
|
152
|
-
{getDisplayUrl(activationResult.tenantId)}
|
|
153
|
-
</p>
|
|
154
|
-
</div>
|
|
155
|
-
)}
|
|
156
|
-
|
|
157
|
-
<div class="space-y-4">
|
|
158
|
-
{activationResult.tenantId ? (
|
|
159
|
-
<a
|
|
160
|
-
href={getDashboardUrl(activationResult.tenantId)}
|
|
161
|
-
class="inline-block rounded-lg bg-orange-600 px-6 py-3 font-bold text-white transition-colors hover:bg-orange-700"
|
|
162
|
-
id="dashboard-button"
|
|
163
|
-
>
|
|
164
|
-
Access Your Dashboard
|
|
165
|
-
</a>
|
|
166
|
-
) : (
|
|
167
|
-
<div class="text-gray-500">
|
|
168
|
-
<p class="mb-2">Unable to determine tenant URL</p>
|
|
169
|
-
<p class="text-sm">Please contact support</p>
|
|
170
|
-
</div>
|
|
171
|
-
)}
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
) : (
|
|
175
|
-
<div class="rounded-lg bg-white p-8 text-center shadow-lg">
|
|
176
|
-
<div class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-red-100">
|
|
177
|
-
<svg
|
|
178
|
-
class="h-8 w-8 text-red-600"
|
|
179
|
-
fill="none"
|
|
180
|
-
stroke="currentColor"
|
|
181
|
-
viewBox="0 0 24 24"
|
|
182
|
-
>
|
|
183
|
-
<path
|
|
184
|
-
stroke-linecap="round"
|
|
185
|
-
stroke-linejoin="round"
|
|
186
|
-
stroke-width="2"
|
|
187
|
-
d="M6 18L18 6M6 6l12 12"
|
|
188
|
-
/>
|
|
189
|
-
</svg>
|
|
190
|
-
</div>
|
|
191
|
-
|
|
192
|
-
<h2 class="mb-4 text-2xl font-bold text-gray-900">
|
|
193
|
-
Activation Failed
|
|
194
|
-
</h2>
|
|
195
|
-
|
|
196
|
-
<p class="mb-6 text-gray-600">{activationResult.error}</p>
|
|
197
|
-
|
|
198
|
-
<div class="mb-6 rounded-lg border border-yellow-200 bg-yellow-50 p-4">
|
|
199
|
-
<h3 class="mb-2 text-sm font-bold text-yellow-800">
|
|
200
|
-
Common Issues:
|
|
201
|
-
</h3>
|
|
202
|
-
<ul class="space-y-1 text-left text-sm text-yellow-700">
|
|
203
|
-
<li>• The activation link may have expired</li>
|
|
204
|
-
<li>• The token may have already been used</li>
|
|
205
|
-
<li>• There may be a temporary server issue</li>
|
|
206
|
-
</ul>
|
|
207
|
-
</div>
|
|
208
|
-
|
|
209
|
-
<div class="space-y-4">
|
|
210
|
-
<a
|
|
211
|
-
href="/sandbox/register"
|
|
212
|
-
class="inline-block rounded-lg bg-orange-600 px-6 py-3 font-bold text-white transition-colors hover:bg-orange-700"
|
|
213
|
-
>
|
|
214
|
-
Register New Tenant
|
|
215
|
-
</a>
|
|
216
|
-
|
|
217
|
-
<div class="text-center">
|
|
218
|
-
<a
|
|
219
|
-
href="mailto:support@tractstack.com"
|
|
220
|
-
class="text-sm text-orange-600 underline hover:text-orange-800"
|
|
221
|
-
>
|
|
222
|
-
Contact Support
|
|
223
|
-
</a>
|
|
224
|
-
</div>
|
|
225
|
-
</div>
|
|
226
|
-
</div>
|
|
227
|
-
)
|
|
228
|
-
}
|
|
229
|
-
</div>
|
|
230
|
-
</main>
|
|
231
|
-
</Layout>
|
|
232
|
-
|
|
233
|
-
<script>
|
|
234
|
-
// Auto-redirect to dashboard after successful activation with countdown
|
|
235
|
-
const dashboardButton = document.getElementById('dashboard-button');
|
|
236
|
-
if (dashboardButton) {
|
|
237
|
-
let countdown = 5;
|
|
238
|
-
|
|
239
|
-
// Update button text with countdown
|
|
240
|
-
const updateButton = () => {
|
|
241
|
-
dashboardButton.textContent = `Access Your Dashboard (${countdown}s)`;
|
|
242
|
-
countdown--;
|
|
243
|
-
|
|
244
|
-
if (countdown < 0) {
|
|
245
|
-
dashboardButton.click();
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
// Start countdown immediately
|
|
250
|
-
updateButton();
|
|
251
|
-
const interval = setInterval(updateButton, 1000);
|
|
252
|
-
|
|
253
|
-
// Clear interval if user clicks button manually
|
|
254
|
-
dashboardButton.addEventListener('click', () => {
|
|
255
|
-
clearInterval(interval);
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
</script>
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import Layout from '@/layouts/Layout.astro';
|
|
3
|
-
import RegistrationForm from '@/components/tenant/RegistrationForm';
|
|
4
|
-
import { getBrandConfig } from '@/utils/api/brandConfig';
|
|
5
|
-
import { preHealthCheck } from '@/utils/backend';
|
|
6
|
-
|
|
7
|
-
const tenantId = 'default'; // For registration, always use default
|
|
8
|
-
|
|
9
|
-
const healthCheckRedirect = await preHealthCheck(tenantId);
|
|
10
|
-
if (healthCheckRedirect !== undefined) {
|
|
11
|
-
return healthCheckRedirect;
|
|
12
|
-
}
|
|
13
|
-
// Check if multi-tenant is enabled
|
|
14
|
-
const isMultiTenantEnabled =
|
|
15
|
-
import.meta.env.PUBLIC_ENABLE_MULTI_TENANT === 'true';
|
|
16
|
-
if (!isMultiTenantEnabled) {
|
|
17
|
-
return Astro.redirect('/storykeep?error=multi-tenant-disabled');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const brandConfig = await getBrandConfig('default');
|
|
21
|
-
const title = 'Register New Tenant | TractStack';
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
<Layout title={title} slug="sandbox-register" brandConfig={brandConfig}>
|
|
25
|
-
<main id="main-content" class="min-h-screen bg-gray-50">
|
|
26
|
-
<div class="py-12">
|
|
27
|
-
<RegistrationForm
|
|
28
|
-
client:load
|
|
29
|
-
onSuccess={(tenantId) => {
|
|
30
|
-
if (tenantId)
|
|
31
|
-
window.location.href = `/sandbox/success?tenant=${encodeURIComponent(tenantId)}`;
|
|
32
|
-
}}
|
|
33
|
-
onCapacityFull={() => {
|
|
34
|
-
window.location.href = '/sandbox/capacity-full';
|
|
35
|
-
}}
|
|
36
|
-
/>
|
|
37
|
-
</div>
|
|
38
|
-
</main>
|
|
39
|
-
</Layout>
|
|
40
|
-
|
|
41
|
-
<script>
|
|
42
|
-
// Add any additional client-side logic here if needed
|
|
43
|
-
console.log('Tenant registration page loaded');
|
|
44
|
-
</script>
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import Layout from '@/layouts/Layout.astro';
|
|
3
|
-
import { getBrandConfig } from '@/utils/api/brandConfig';
|
|
4
|
-
import { preHealthCheck } from '@/utils/backend';
|
|
5
|
-
|
|
6
|
-
// Get tenant ID from URL params
|
|
7
|
-
const url = new URL(Astro.request.url);
|
|
8
|
-
const tenantId = url.searchParams.get('tenant');
|
|
9
|
-
|
|
10
|
-
const healthCheckRedirect = await preHealthCheck(tenantId || `default`);
|
|
11
|
-
if (healthCheckRedirect !== undefined) {
|
|
12
|
-
return healthCheckRedirect;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Check if multi-tenant is enabled
|
|
16
|
-
const isMultiTenantEnabled =
|
|
17
|
-
import.meta.env.PUBLIC_ENABLE_MULTI_TENANT === 'true';
|
|
18
|
-
if (!isMultiTenantEnabled) {
|
|
19
|
-
return Astro.redirect('/?error=multi-tenant-disabled');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (!tenantId) {
|
|
23
|
-
return Astro.redirect('/sandbox/register?error=missing-tenant-id');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Get backend URL
|
|
27
|
-
const brandConfig = await getBrandConfig('default');
|
|
28
|
-
const title = 'Registration Successful | TractStack';
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
<Layout title={title} slug="tenant-success" brandConfig={brandConfig}>
|
|
32
|
-
<main
|
|
33
|
-
id="main-content"
|
|
34
|
-
class="flex min-h-screen items-center justify-center bg-gray-50"
|
|
35
|
-
>
|
|
36
|
-
<div class="mx-auto max-w-2xl p-6">
|
|
37
|
-
<div class="rounded-lg bg-white p-8 text-center shadow-lg">
|
|
38
|
-
<div
|
|
39
|
-
class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-orange-100"
|
|
40
|
-
>
|
|
41
|
-
<svg
|
|
42
|
-
class="h-8 w-8 text-orange-600"
|
|
43
|
-
fill="none"
|
|
44
|
-
stroke="currentColor"
|
|
45
|
-
viewBox="0 0 24 24"
|
|
46
|
-
>
|
|
47
|
-
<path
|
|
48
|
-
stroke-linecap="round"
|
|
49
|
-
stroke-linejoin="round"
|
|
50
|
-
stroke-width="2"
|
|
51
|
-
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
|
52
|
-
></path>
|
|
53
|
-
</svg>
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
<h2 class="mb-4 text-2xl font-bold text-gray-900">
|
|
57
|
-
📧 Check Your Email
|
|
58
|
-
</h2>
|
|
59
|
-
|
|
60
|
-
<p class="mb-6 text-gray-600">
|
|
61
|
-
Your tenant <strong class="font-mono text-orange-600"
|
|
62
|
-
>{tenantId}</strong
|
|
63
|
-
> has been created successfully!
|
|
64
|
-
</p>
|
|
65
|
-
|
|
66
|
-
<div class="mb-6 rounded-lg border border-orange-200 bg-orange-50 p-6">
|
|
67
|
-
<h3 class="mb-3 text-lg font-bold text-orange-800">
|
|
68
|
-
What happens next:
|
|
69
|
-
</h3>
|
|
70
|
-
<div class="space-y-3 text-left">
|
|
71
|
-
<div class="flex items-start space-x-3">
|
|
72
|
-
<div
|
|
73
|
-
class="mt-0.5 flex h-6 w-6 items-center justify-center rounded-full bg-orange-600 text-sm font-bold text-white"
|
|
74
|
-
>
|
|
75
|
-
1
|
|
76
|
-
</div>
|
|
77
|
-
<div>
|
|
78
|
-
<p class="font-bold text-orange-800">Check your email inbox</p>
|
|
79
|
-
<p class="text-sm text-orange-600">
|
|
80
|
-
We've sent an activation link to complete setup
|
|
81
|
-
</p>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
|
|
85
|
-
<div class="flex items-start space-x-3">
|
|
86
|
-
<div
|
|
87
|
-
class="mt-0.5 flex h-6 w-6 items-center justify-center rounded-full bg-orange-600 text-sm font-bold text-white"
|
|
88
|
-
>
|
|
89
|
-
2
|
|
90
|
-
</div>
|
|
91
|
-
<div>
|
|
92
|
-
<p class="font-bold text-orange-800">
|
|
93
|
-
Click the activation link
|
|
94
|
-
</p>
|
|
95
|
-
<p class="text-sm text-orange-600">
|
|
96
|
-
This will finalize your tenant setup
|
|
97
|
-
</p>
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
|
|
101
|
-
<div class="flex items-start space-x-3">
|
|
102
|
-
<div
|
|
103
|
-
class="mt-0.5 flex h-6 w-6 items-center justify-center rounded-full bg-orange-600 text-sm font-bold text-white"
|
|
104
|
-
>
|
|
105
|
-
3
|
|
106
|
-
</div>
|
|
107
|
-
<div>
|
|
108
|
-
<p class="font-bold text-orange-800">Access your dashboard</p>
|
|
109
|
-
<p class="text-sm text-orange-600">
|
|
110
|
-
Visit <strong class="font-mono"
|
|
111
|
-
>{tenantId}.sandbox.freewebpress.com</strong
|
|
112
|
-
>
|
|
113
|
-
</p>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
<div class="mb-6 rounded-lg border border-yellow-200 bg-yellow-50 p-4">
|
|
120
|
-
<p class="text-sm text-yellow-800">
|
|
121
|
-
<strong>⏰ Important:</strong> The activation link will expire in 48
|
|
122
|
-
hours. If you don't see the email, check your spam folder.
|
|
123
|
-
</p>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
<div class="space-y-4">
|
|
127
|
-
<div class="text-center">
|
|
128
|
-
<p class="mb-4 text-sm text-gray-500">
|
|
129
|
-
Once activated, your tenant URL will be:
|
|
130
|
-
</p>
|
|
131
|
-
<p
|
|
132
|
-
class="break-all rounded border bg-gray-100 p-3 font-mono text-lg"
|
|
133
|
-
>
|
|
134
|
-
https://{tenantId}.sandbox.freewebpress.com
|
|
135
|
-
</p>
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
<div class="flex flex-col justify-center gap-4 sm:flex-row">
|
|
139
|
-
<a
|
|
140
|
-
href="/sandbox/register"
|
|
141
|
-
class="rounded-lg border border-gray-300 px-6 py-2 text-gray-700 transition-colors hover:bg-gray-50"
|
|
142
|
-
>
|
|
143
|
-
Register Another Tenant
|
|
144
|
-
</a>
|
|
145
|
-
|
|
146
|
-
<a
|
|
147
|
-
href="https://docs.tractstack.com"
|
|
148
|
-
class="rounded-lg bg-orange-600 px-6 py-2 text-white transition-colors hover:bg-orange-700"
|
|
149
|
-
target="_blank"
|
|
150
|
-
rel="noopener noreferrer"
|
|
151
|
-
>
|
|
152
|
-
View Documentation
|
|
153
|
-
</a>
|
|
154
|
-
</div>
|
|
155
|
-
|
|
156
|
-
<div class="border-t pt-4 text-center">
|
|
157
|
-
<p class="text-sm text-gray-500">
|
|
158
|
-
Need help?
|
|
159
|
-
<a
|
|
160
|
-
href="mailto:support@tractstack.com"
|
|
161
|
-
class="text-orange-600 underline hover:text-orange-800"
|
|
162
|
-
>
|
|
163
|
-
Contact Support
|
|
164
|
-
</a>
|
|
165
|
-
</p>
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
170
|
-
</main>
|
|
171
|
-
</Layout>
|
|
172
|
-
|
|
173
|
-
<script>
|
|
174
|
-
// Add email resend functionality (placeholder for future implementation)
|
|
175
|
-
console.log(
|
|
176
|
-
'Registration success page loaded for tenant:',
|
|
177
|
-
document.querySelector('[data-tenant-id]')?.textContent
|
|
178
|
-
);
|
|
179
|
-
</script>
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// Tenant configuration API utilities following TractStack v2 patterns
|
|
2
|
-
import { TractStackAPI } from '../api';
|
|
3
|
-
import type {
|
|
4
|
-
TenantProvisioningData,
|
|
5
|
-
TenantCapacity,
|
|
6
|
-
TenantProvisioningResponse,
|
|
7
|
-
} from '@/types/multiTenant';
|
|
8
|
-
import type { TenantActivationRequest } from '@/types/multiTenant';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Check tenant capacity and existing tenants
|
|
12
|
-
*/
|
|
13
|
-
export async function checkTenantCapacity(
|
|
14
|
-
tenantId: string
|
|
15
|
-
): Promise<TenantCapacity> {
|
|
16
|
-
const api = new TractStackAPI(tenantId);
|
|
17
|
-
try {
|
|
18
|
-
const response = await api.get<TenantCapacity>('/api/v1/tenant/capacity');
|
|
19
|
-
|
|
20
|
-
if (!response.success || !response.data) {
|
|
21
|
-
throw new Error(response.error || 'No data received from server');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const data = response.data;
|
|
25
|
-
|
|
26
|
-
// Validate response structure to match backend
|
|
27
|
-
if (
|
|
28
|
-
typeof data.available !== 'boolean' ||
|
|
29
|
-
typeof data.currentTenants !== 'number' ||
|
|
30
|
-
typeof data.maxTenants !== 'number' ||
|
|
31
|
-
typeof data.availableSlots !== 'number'
|
|
32
|
-
) {
|
|
33
|
-
throw new Error('Invalid response format from server');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return data;
|
|
37
|
-
} catch (error) {
|
|
38
|
-
console.error('Failed to fetch tenant capacity:', error);
|
|
39
|
-
throw new Error('Failed to load tenant capacity information');
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Provision a new tenant with reserved status
|
|
45
|
-
*/
|
|
46
|
-
export async function provisionTenant(
|
|
47
|
-
tenantId: string,
|
|
48
|
-
data: TenantProvisioningData
|
|
49
|
-
): Promise<TenantProvisioningResponse> {
|
|
50
|
-
const api = new TractStackAPI(tenantId);
|
|
51
|
-
try {
|
|
52
|
-
const response = await api.post<TenantProvisioningResponse>(
|
|
53
|
-
'/api/v1/tenant/provision',
|
|
54
|
-
data
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
if (!response.success || !response.data) {
|
|
58
|
-
throw new Error(response.error || 'Failed to provision tenant');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return response.data;
|
|
62
|
-
} catch (error) {
|
|
63
|
-
console.error('Failed to provision tenant:', error);
|
|
64
|
-
|
|
65
|
-
if (error instanceof Error) {
|
|
66
|
-
throw error;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
throw new Error('Failed to provision tenant');
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Activate a tenant using the activation token
|
|
75
|
-
*/
|
|
76
|
-
export async function activateTenant(
|
|
77
|
-
tenantId: string,
|
|
78
|
-
token: string
|
|
79
|
-
): Promise<void> {
|
|
80
|
-
const api = new TractStackAPI(tenantId);
|
|
81
|
-
try {
|
|
82
|
-
const request: TenantActivationRequest = { token };
|
|
83
|
-
const response = await api.post('/api/v1/activate-tenant', request);
|
|
84
|
-
|
|
85
|
-
if (!response.success) {
|
|
86
|
-
throw new Error(response.error || 'Failed to activate tenant');
|
|
87
|
-
}
|
|
88
|
-
} catch (error) {
|
|
89
|
-
console.error('Failed to activate tenant:', error);
|
|
90
|
-
|
|
91
|
-
if (error instanceof Error) {
|
|
92
|
-
throw error;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
throw new Error('Failed to activate tenant');
|
|
96
|
-
}
|
|
97
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
// Tenant form validation and state helpers following TractStack v2 patterns
|
|
2
|
-
import type { TenantProvisioningData } from '@/types/multiTenant';
|
|
3
|
-
import type {
|
|
4
|
-
TenantRegistrationState,
|
|
5
|
-
TenantValidationErrors,
|
|
6
|
-
} from '@/types/multiTenant';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Convert tenant provisioning data to local form state
|
|
10
|
-
*/
|
|
11
|
-
export function convertToLocalState(
|
|
12
|
-
data?: TenantProvisioningData
|
|
13
|
-
): TenantRegistrationState {
|
|
14
|
-
if (!data) {
|
|
15
|
-
return {
|
|
16
|
-
tenantId: '',
|
|
17
|
-
adminPassword: '',
|
|
18
|
-
confirmPassword: '',
|
|
19
|
-
name: '',
|
|
20
|
-
email: '',
|
|
21
|
-
tursoEnabled: false,
|
|
22
|
-
tursoDatabaseURL: '',
|
|
23
|
-
tursoAuthToken: '',
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
tenantId: data.tenantId,
|
|
29
|
-
adminPassword: data.adminPassword,
|
|
30
|
-
confirmPassword: '',
|
|
31
|
-
name: data.name,
|
|
32
|
-
email: data.adminEmail,
|
|
33
|
-
tursoEnabled: data.tursoEnabled,
|
|
34
|
-
tursoDatabaseURL: data.tursoDatabaseURL || '',
|
|
35
|
-
tursoAuthToken: data.tursoAuthToken || '',
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Convert local form state to backend format
|
|
41
|
-
*/
|
|
42
|
-
export function convertToBackendFormat(
|
|
43
|
-
state: TenantRegistrationState
|
|
44
|
-
): TenantProvisioningData {
|
|
45
|
-
const data: TenantProvisioningData = {
|
|
46
|
-
tenantId: state.tenantId.trim(),
|
|
47
|
-
adminPassword: state.adminPassword.trim(),
|
|
48
|
-
name: state.name.trim(),
|
|
49
|
-
adminEmail: state.email.trim(),
|
|
50
|
-
tursoEnabled: state.tursoEnabled,
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Only include Turso credentials if enabled
|
|
54
|
-
if (state.tursoEnabled) {
|
|
55
|
-
data.tursoDatabaseURL = state.tursoDatabaseURL.trim();
|
|
56
|
-
data.tursoAuthToken = state.tursoAuthToken.trim();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return data;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Validate tenant registration form
|
|
64
|
-
*/
|
|
65
|
-
export function validateTenantRegistration(
|
|
66
|
-
state: TenantRegistrationState,
|
|
67
|
-
existingTenants?: string[],
|
|
68
|
-
isInitMode?: boolean
|
|
69
|
-
): TenantValidationErrors {
|
|
70
|
-
const errors: TenantValidationErrors = {};
|
|
71
|
-
|
|
72
|
-
// Skip ALL tenant ID validation in init mode
|
|
73
|
-
if (!isInitMode) {
|
|
74
|
-
const tenantId = state.tenantId.trim();
|
|
75
|
-
if (!tenantId) {
|
|
76
|
-
errors.tenantId = 'Tenant ID is required';
|
|
77
|
-
} else if (tenantId.length < 3 || tenantId.length > 12) {
|
|
78
|
-
errors.tenantId = 'Tenant ID must be 3-12 characters long';
|
|
79
|
-
} else if (tenantId !== tenantId.toLowerCase()) {
|
|
80
|
-
errors.tenantId = 'Tenant ID must be lowercase';
|
|
81
|
-
} else if (!/^[a-z0-9-]+$/.test(tenantId)) {
|
|
82
|
-
errors.tenantId =
|
|
83
|
-
'Tenant ID can only contain lowercase letters, numbers, and dashes';
|
|
84
|
-
} else if (tenantId === 'default') {
|
|
85
|
-
errors.tenantId = "'default' is a reserved tenant ID";
|
|
86
|
-
} else if (existingTenants && existingTenants.includes(tenantId)) {
|
|
87
|
-
errors.tenantId = 'This tenant ID is already taken';
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Admin password validation
|
|
92
|
-
if (!state.adminPassword.trim()) {
|
|
93
|
-
errors.adminPassword = 'Admin password is required';
|
|
94
|
-
} else if (state.adminPassword.length < 8) {
|
|
95
|
-
errors.adminPassword = 'Admin password must be at least 8 characters long';
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Password confirmation validation - only if main password is valid
|
|
99
|
-
if (!errors.adminPassword && state.adminPassword !== state.confirmPassword) {
|
|
100
|
-
errors.confirmPassword = 'Passwords do not match';
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Name validation
|
|
104
|
-
if (!state.name.trim()) {
|
|
105
|
-
errors.name = 'Name is required';
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Email validation
|
|
109
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
110
|
-
if (!state.email.trim()) {
|
|
111
|
-
errors.email = 'Email is required';
|
|
112
|
-
} else if (!emailRegex.test(state.email.trim())) {
|
|
113
|
-
errors.email = 'Please enter a valid email address';
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Turso validation (if enabled)
|
|
117
|
-
if (state.tursoEnabled) {
|
|
118
|
-
if (!state.tursoDatabaseURL.trim()) {
|
|
119
|
-
errors.tursoDatabaseURL =
|
|
120
|
-
'Turso Database URL is required when Turso is enabled';
|
|
121
|
-
} else if (!state.tursoDatabaseURL.startsWith('libsql://')) {
|
|
122
|
-
errors.tursoDatabaseURL =
|
|
123
|
-
'Turso Database URL must start with "libsql://"';
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (!state.tursoAuthToken.trim()) {
|
|
127
|
-
errors.tursoAuthToken =
|
|
128
|
-
'Turso Auth Token is required when Turso is enabled';
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return errors;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* State interceptor for cross-field logic
|
|
137
|
-
*/
|
|
138
|
-
export function tenantStateIntercept(
|
|
139
|
-
newState: TenantRegistrationState,
|
|
140
|
-
field: keyof TenantRegistrationState,
|
|
141
|
-
value: any
|
|
142
|
-
): TenantRegistrationState {
|
|
143
|
-
// Clear Turso fields when disabled
|
|
144
|
-
if (field === 'tursoEnabled' && !value) {
|
|
145
|
-
return {
|
|
146
|
-
...newState,
|
|
147
|
-
tursoEnabled: false,
|
|
148
|
-
tursoDatabaseURL: '',
|
|
149
|
-
tursoAuthToken: '',
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Clear confirmation password when main password changes
|
|
154
|
-
if (field === 'adminPassword') {
|
|
155
|
-
return {
|
|
156
|
-
...newState,
|
|
157
|
-
adminPassword: value,
|
|
158
|
-
confirmPassword: '', // Clear confirmation to force re-entry
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Normalize tenant ID
|
|
163
|
-
if (field === 'tenantId' && typeof value === 'string') {
|
|
164
|
-
return {
|
|
165
|
-
...newState,
|
|
166
|
-
tenantId: value.toLowerCase().replace(/[^a-z0-9-]/g, ''),
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Default behavior
|
|
171
|
-
return newState;
|
|
172
|
-
}
|