Skip to content

Commit 2f8f5e2

Browse files
fix: add validation for Turso DB credentials and handle undefined values (#12)
Co-authored-by: Adam Matthiesen <[email protected]>
1 parent 3bd4461 commit 2f8f5e2

File tree

5 files changed

+230
-7
lines changed

5 files changed

+230
-7
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-studiocms": patch
3+
---
4+
5+
Fixes undefined Turso DB credentials in environment files and add validation

package/src/cmds/interactive/data/studiocmsenv.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ export function buildEnvFile(envBuilderOpts: EnvBuilderOptions): string {
3838
let envFileContent = `# StudioCMS Environment Variables
3939
4040
# libSQL URL and Token for AstroDB
41-
ASTRO_DB_REMOTE_URL=${envBuilderOpts.astroDbRemoteUrl}
42-
ASTRO_DB_APP_TOKEN=${envBuilderOpts.astroDbToken}
41+
ASTRO_DB_REMOTE_URL=${envBuilderOpts.astroDbRemoteUrl || ''}
42+
ASTRO_DB_APP_TOKEN=${envBuilderOpts.astroDbToken || ''}
4343
4444
# Auth encryption key
4545
CMS_ENCRYPTION_KEY="${envBuilderOpts.encryptionKey}" # openssl rand --base64 16

package/src/cmds/interactive/envBuilder.ts

Lines changed: 207 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,107 @@ export async function env(
267267

268268
ctx.debug && ctx.logger.debug(`Database Token: ${dbToken}`);
269269

270-
envBuilderOpts.astroDbRemoteUrl = dbURL;
271-
envBuilderOpts.astroDbToken = dbToken;
270+
// Validate URL and token
271+
if (!dbURL || !dbURL.startsWith('libsql://')) {
272+
tursoSetup.stop(
273+
`${label('Turso', TursoColorway, color.black)} Failed to retrieve a valid database URL.`
274+
);
275+
ctx.prompt.log.error(StudioCMSColorwayError(`Invalid database URL: ${dbURL || 'undefined'}`));
276+
277+
const manualURL = await ctx.prompt.text({
278+
message: 'Enter your Turso database URL manually',
279+
placeholder: 'libsql://your-database.turso.io',
280+
});
281+
282+
if (typeof manualURL === 'symbol') {
283+
ctx.promptCancel(manualURL);
284+
} else {
285+
envBuilderOpts.astroDbRemoteUrl = manualURL || '';
286+
}
287+
} else {
288+
envBuilderOpts.astroDbRemoteUrl = dbURL;
289+
}
290+
291+
if (!dbToken || dbToken.length < 10) {
292+
tursoSetup.stop(
293+
`${label('Turso', TursoColorway, color.black)} Failed to retrieve a valid token.`
294+
);
295+
ctx.prompt.log.error(StudioCMSColorwayError(`Invalid database token: ${dbToken ? 'too short' : 'undefined'}`));
296+
297+
const manualToken = await ctx.prompt.text({
298+
message: 'Enter your Turso database token manually',
299+
placeholder: 'eyJh...Nzc2',
300+
});
301+
302+
if (typeof manualToken === 'symbol') {
303+
ctx.promptCancel(manualToken);
304+
} else {
305+
envBuilderOpts.astroDbToken = manualToken || '';
306+
}
307+
} else {
308+
envBuilderOpts.astroDbToken = dbToken;
309+
}
310+
311+
// Verify credentials with a simple test connection
312+
if (envBuilderOpts.astroDbRemoteUrl && envBuilderOpts.astroDbToken) {
313+
tursoSetup.message(
314+
`${label('Turso', TursoColorway, color.black)} Verifying database connection...`
315+
);
316+
317+
try {
318+
// Test connection using curl (doesn't require additional dependencies)
319+
const connectionTest = await runShellCommand(
320+
`curl -s -o /dev/null -w "%{http_code}" ${envBuilderOpts.astroDbRemoteUrl}/health -H "Authorization: Bearer ${envBuilderOpts.astroDbToken}"`
321+
);
322+
323+
const statusCode = Number.parseInt(connectionTest.trim(), 10);
324+
325+
if (statusCode >= 200 && statusCode < 300) {
326+
ctx.debug && ctx.logger.debug(`Database connection successful: ${statusCode}`);
327+
} else {
328+
ctx.debug && ctx.logger.debug(`Database connection failed: ${statusCode}`);
329+
ctx.prompt.log.warn(
330+
`${label('Warning', StudioCMSColorwayWarnBg, color.black)} Could not verify database connection. Status: ${statusCode}`
331+
);
332+
333+
const confirmContinue = await ctx.prompt.confirm({
334+
message: 'Continue with these credentials anyway?',
335+
initialValue: true,
336+
});
337+
338+
if (typeof confirmContinue === 'symbol') {
339+
ctx.promptCancel(confirmContinue);
340+
} else if (!confirmContinue) {
341+
// If user doesn't want to continue with unverified credentials, ask for new ones
342+
const newCredentials = await ctx.prompt.group(
343+
{
344+
astroDbRemoteUrl: () =>
345+
ctx.prompt.text({
346+
message: 'Remote URL for AstroDB',
347+
initialValue: envBuilderOpts.astroDbRemoteUrl || 'libsql://your-database.turso.io',
348+
}),
349+
astroDbToken: () =>
350+
ctx.prompt.text({
351+
message: 'AstroDB Token',
352+
initialValue: '',
353+
}),
354+
},
355+
{
356+
onCancel: () => ctx.promptOnCancel(),
357+
}
358+
);
359+
360+
envBuilderOpts.astroDbRemoteUrl = newCredentials.astroDbRemoteUrl || '';
361+
envBuilderOpts.astroDbToken = newCredentials.astroDbToken || '';
362+
}
363+
}
364+
} catch (error) {
365+
ctx.debug && ctx.logger.debug(`Database connection test error: ${error instanceof Error ? error.message : 'unknown error'}`);
366+
ctx.prompt.log.warn(
367+
`${label('Warning', StudioCMSColorwayWarnBg, color.black)} Could not verify database connection due to an error.`
368+
);
369+
}
370+
}
272371

273372
tursoSetup.stop(
274373
`${label('Turso', TursoColorway, color.black)} Database setup complete. New Database: ${dbFinalName}`
@@ -312,7 +411,100 @@ export async function env(
312411

313412
ctx.debug && ctx.logger.debug(`AstroDB setup: ${envBuilderStep_AstroDB}`);
314413

315-
envBuilderOpts = { ...envBuilderStep_AstroDB };
414+
// Validate the manually entered credentials
415+
let dbUrl = envBuilderStep_AstroDB.astroDbRemoteUrl || '';
416+
let dbToken = envBuilderStep_AstroDB.astroDbToken || '';
417+
418+
// Check URL format
419+
if (!dbUrl.startsWith('libsql://') && dbUrl !== '') {
420+
ctx.prompt.log.warn(
421+
`${label('Warning', StudioCMSColorwayWarnBg, color.black)} The database URL should start with 'libsql://'.`
422+
);
423+
424+
const fixUrl = await ctx.prompt.confirm({
425+
message: 'Would you like to prepend "libsql://" to your URL?',
426+
initialValue: true,
427+
});
428+
429+
if (typeof fixUrl === 'symbol') {
430+
ctx.promptCancel(fixUrl);
431+
} else if (fixUrl) {
432+
dbUrl = `libsql://${dbUrl}`;
433+
}
434+
}
435+
436+
// Verify the credentials with a connection test
437+
if (dbUrl && dbToken && dbToken !== 'your-astrodb-token') {
438+
const verifyConnection = await ctx.prompt.confirm({
439+
message: 'Would you like to verify these credentials?',
440+
initialValue: true,
441+
});
442+
443+
if (typeof verifyConnection === 'symbol') {
444+
ctx.promptCancel(verifyConnection);
445+
} else if (verifyConnection) {
446+
const connectionTestSpinner = ctx.prompt.spinner();
447+
connectionTestSpinner.start(`${label('Turso', TursoColorway, color.black)} Verifying database connection...`);
448+
449+
try {
450+
// Test connection using curl (doesn't require additional dependencies)
451+
const connectionTest = await runShellCommand(
452+
`curl -s -o /dev/null -w "%{http_code}" ${dbUrl}/health -H "Authorization: Bearer ${dbToken}"`
453+
);
454+
455+
const statusCode = Number.parseInt(connectionTest.trim(), 10);
456+
457+
if (statusCode >= 200 && statusCode < 300) {
458+
connectionTestSpinner.stop(`${label('Turso', TursoColorway, color.black)} Connection successful!`);
459+
} else {
460+
connectionTestSpinner.stop(`${label('Turso', TursoColorway, color.black)} Connection failed (${statusCode}).`);
461+
ctx.prompt.log.warn(
462+
`${label('Warning', StudioCMSColorwayWarnBg, color.black)} Could not verify database connection. Status: ${statusCode}`
463+
);
464+
465+
const retryCredentials = await ctx.prompt.confirm({
466+
message: 'Would you like to enter different credentials?',
467+
initialValue: true,
468+
});
469+
470+
if (typeof retryCredentials === 'symbol') {
471+
ctx.promptCancel(retryCredentials);
472+
} else if (retryCredentials) {
473+
const newCredentials = await ctx.prompt.group(
474+
{
475+
astroDbRemoteUrl: () =>
476+
ctx.prompt.text({
477+
message: 'Remote URL for AstroDB',
478+
initialValue: dbUrl,
479+
}),
480+
astroDbToken: () =>
481+
ctx.prompt.text({
482+
message: 'AstroDB Token',
483+
initialValue: '',
484+
}),
485+
},
486+
{
487+
onCancel: () => ctx.promptOnCancel(),
488+
}
489+
);
490+
491+
dbUrl = newCredentials.astroDbRemoteUrl || '';
492+
dbToken = newCredentials.astroDbToken || '';
493+
}
494+
}
495+
} catch (error) {
496+
connectionTestSpinner.stop(`${label('Turso', TursoColorway, color.black)} Connection test failed.`);
497+
ctx.debug && ctx.logger.debug(`Database connection test error: ${error instanceof Error ? error.message : 'unknown error'}`);
498+
ctx.prompt.log.warn(
499+
`${label('Warning', StudioCMSColorwayWarnBg, color.black)} Could not verify database connection due to an error.`
500+
);
501+
}
502+
}
503+
}
504+
505+
// Save the validated credentials
506+
envBuilderOpts.astroDbRemoteUrl = dbUrl;
507+
envBuilderOpts.astroDbToken = dbToken;
316508
}
317509
}
318510

@@ -344,7 +536,18 @@ export async function env(
344536

345537
ctx.debug && ctx.logger.debug(`Environment Builder Step 1: ${envBuilderStep1}`);
346538

347-
envBuilderOpts = { ...envBuilderStep1 };
539+
// Preserve AstroDB URL and token while merging
540+
const previousDbValues = {
541+
astroDbRemoteUrl: envBuilderOpts.astroDbRemoteUrl,
542+
astroDbToken: envBuilderOpts.astroDbToken
543+
};
544+
545+
envBuilderOpts = {
546+
...envBuilderOpts,
547+
...envBuilderStep1,
548+
astroDbRemoteUrl: previousDbValues.astroDbRemoteUrl || '',
549+
astroDbToken: previousDbValues.astroDbToken || ''
550+
};
348551

349552
if (envBuilderStep1.oAuthOptions.includes('github')) {
350553
const githubOAuth = await ctx.prompt.group(

package/tests/env-builder.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,19 @@ describe('Environment Builder Configuration', () => {
121121
expect(envWithMultiple).toContain('CMS_ENCRYPTION_KEY="test-encryption-key"');
122122
expect(envWithMultiple).toContain('CMS_CLOUDINARY_CLOUDNAME="demo"');
123123
});
124+
125+
it('handles undefined AstroDB values correctly', () => {
126+
// Test with undefined AstroDB values
127+
const envWithUndefinedValues = buildEnvFile({
128+
encryptionKey: 'test-encryption-key',
129+
oAuthOptions: [],
130+
// Deliberately omit astroDbRemoteUrl and astroDbToken
131+
});
132+
133+
// Verify the environment values are empty strings, not "undefined"
134+
expect(envWithUndefinedValues).toContain('ASTRO_DB_REMOTE_URL=');
135+
expect(envWithUndefinedValues).toContain('ASTRO_DB_APP_TOKEN=');
136+
expect(envWithUndefinedValues).not.toContain('ASTRO_DB_REMOTE_URL=undefined');
137+
expect(envWithUndefinedValues).not.toContain('ASTRO_DB_APP_TOKEN=undefined');
138+
});
124139
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{}
1+
{}

0 commit comments

Comments
 (0)